SPEC v0.1 and project rules for Kvitt

Write SPEC.md v0.1 from the owner interview (requirements, data model,
stack, API outline, security, business rules, deployment, milestones,
UX sprint brief, contracts). Fill the CLAUDE.md blanks, move the Gradle
pin files into android/, point the tools at android/ and server/, and
record the git bootstrap exception in the handoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 20:39:02 +02:00
parent 8bef22afb2
commit cab3e935d6
12 changed files with 2412 additions and 48 deletions
+11
View File
@@ -16,3 +16,14 @@ export/
.claude/worktrees/
# macOS
.DS_Store
# Rust build output
target/
# Server data, keys and certificates (they live on the Pi or in infra/secrets/, never in git)
infra/secrets/
*.sqlite
*.sqlite3
*.db
*.pem
*service-account*.json
google-services.json
*.apk
+37 -19
View File
@@ -2,7 +2,7 @@
Operating guide for AI agents and humans working on **Kvitt**. This is the only working-rules file; it says how to work here, not what the product is. For the product, read `SPEC.md`. It was seeded on 2026-10-17 from the Ebb project's rules after a year of running Opus agents in parallel; keep what fits, delete what does not, but keep one file.
> **Fill in before the first plan:** the one-paragraph product description, the module layout under "Repository layout", the application ids under "The phone" (if this is an Android app), and the fast checks in `tools/git-hooks/pre-commit`.
**The product in one paragraph.** Kvitt is a Splitwise-style shared-expense app for exactly two people, the owner (Jacob, admin) and his spouse, in Sweden. A native Android app (Kotlin, Compose) keeps a full offline copy of the household's expense accounts, expenses, settlements and audit log in Room and syncs with a small Rust server (axum, SQLite) that runs in Docker on a Raspberry Pi 3 at home, reachable at `https://icehack.asuscomm.com:8443`. Money is SEK in integer öre. The phone computes balances, settle-up proposals and spending analytics itself; the server stores, syncs, schedules recurring expenses, sends push notifications and hosts the APK for in-app updates. Everything else is in `SPEC.md`.
---
@@ -20,20 +20,26 @@ Operating guide for AI agents and humans working on **Kvitt**. This is the only
```
SPEC.md, PLAN-*.md what to build; prompts/ holds a ready-to-paste start prompt per plan and the orchestrator prompt
contracts/ openapi.yaml, test-vectors/*.json, sync and push examples: the cross-team contracts (SPEC §14)
docs/HANDOFF.md state of the work and how it is run
docs/decisions.md decisions that changed or refined the spec
docs/ux/ the UX sprint deliverables (SPEC §13)
docs/spikes/, docs/validation/ what the device and the data showed (reports are deliverables)
tools/ gradle-serial.sh, clean-builds.sh, install-git-hooks.sh, dependency-report/latest.py
(root) the app: one Gradle project, modules as in SPEC
android/ the Android app: one Gradle project (settings.gradle.kts, gradle/libs.versions.toml), modules as in SPEC §14.7
server/ the Rust backend: one Cargo workspace, crates kvitt-domain, kvitt-server, kvitt-import (SPEC §14.7)
infra/ docker compose stack for the Pi (caddy, server, cert-sync, backup), image build scripts, ops runbooks
tools/ gradle-serial.sh, clean-builds.sh, install-git-hooks.sh, git-hooks/, dependency-report/latest.py
```
Who owns which directory during parallel work is in SPEC §14.7. An agent edits only the paths its plan owns.
---
## ORCHESTRATION: HOW WORK IS RUN
The owner talks to one **orchestrator** session. The orchestrator plans, briefs, lands, gates, installs and answers; it **never writes code or repository changes itself**, however small (docs and config included). Every change goes through an **Opus** implementation agent. If the orchestrator slips and edits something, it keeps the work and hands it to an agent to own; it never reverts or redoes it.
* **One Opus agent per task, in its own git worktree** (`.claude/worktrees/agent-<id>`, branch of the same name). The agent's first step is `git reset --hard main`, then copying `local.properties` (and any git-ignored vendor binaries) from the main checkout. It commits on its branch, rebases on `main`, runs the gate, and reports; it never lands, pushes or removes its worktree.
* **One Opus agent per task, in its own git worktree** (`.claude/worktrees/agent-<id>`, branch of the same name). The agent's first step is `git reset --hard main`, then copying `android/local.properties`, `android/app/google-services.json` and anything in `infra/secrets/` it needs (all git-ignored) from the main checkout. It commits on its branch, rebases on `main`, runs the gate, and reports; it never lands, pushes or removes its worktree.
* **The orchestrator lands:** `git merge --ff-only <branch>` in the main checkout, re-runs the gate on `main`, installs on the phone if attached, removes the worktree (`git worktree remove --force --force`, falling back to `rm -rf` + `git worktree prune`) and deletes the branch. No merge commits. Never rebase `main`.
* **Briefs are the whole context an agent has.** A brief states: the goal, what to read first (file paths), what is already established, the exact numbers it may use (next free decision, spec version, schema), the setup steps, the gate, what to report. If the orchestrator cannot write a clear brief, it does not understand the task yet.
* **Agents' reports are not shown to the owner.** The orchestrator extracts the decision: one line per finding, fixed or not, and relays every owner step in chat.
@@ -54,13 +60,15 @@ The owner talks to one **orchestrator** session. The orchestrator plans, briefs,
---
## THE PHONE (if this is a device app)
## THE PHONE
Two phones: the owner's Samsung Galaxy S26 and the spouse's Galaxy S26+, both Android 16. Agents only ever touch the phone attached to this MacBook over USB, which is the owner's unless the brief says otherwise.
| App | Application ID |
|---|---|
| Real app, release | `<id>` |
| Real app, debug | `<id>.debug` |
| Instrumented-test APKs | `<id>.<module>.test` |
| Real app, release | `se.hagstedt.kvitt` |
| Real app, debug | `se.hagstedt.kvitt.debug` |
| Instrumented-test APKs | `se.hagstedt.kvitt.<module>.test` |
* `adb` is at `/opt/homebrew/share/android-commandlinetools/platform-tools/adb` on this MacBook; it is not on the shell PATH.
* Use `adb` **only for this project's apps**. Do not browse, copy or change anything else on the device.
@@ -74,24 +82,32 @@ The owner talks to one **orchestrator** session. The orchestrator plans, briefs,
Everything builds **locally on this MacBook**. No hosted CI, no remote build service.
**JDK.** Gradle runs on JDK 25 (current LTS): `gradle/gradle-daemon-jvm.properties` asks for 25 and `gradle.properties` points Gradle at Homebrew's `openjdk@25` (auto-download off). The shell's `java` is a macOS stub or a newer non-LTS: run Gradle as `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh <tasks>` and **never set `JAVA_HOME`**. Do not install another JDK or change the pin without asking.
**Two projects, two gates.** The Android app is the Gradle project in `android/`; the server is the Cargo workspace in `server/`. A change that touches both runs both gates.
**Android SDK.** `local.properties` must contain `sdk.dir` (`/opt/homebrew/share/android-commandlinetools` on this MacBook). Add the line if missing; never overwrite the file, it also holds client ids and tokens; never commit it.
**JDK.** Gradle runs on JDK 25 (current LTS): `android/gradle/gradle-daemon-jvm.properties` asks for 25 and `android/gradle.properties` points Gradle at Homebrew's `openjdk@25` (auto-download off). The shell's `java` is a macOS stub or a newer non-LTS: run Gradle as `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh <tasks>` and **never set `JAVA_HOME`**. Do not install another JDK or change the pin without asking.
**Android SDK.** `android/local.properties` must contain `sdk.dir` (`/opt/homebrew/share/android-commandlinetools` on this MacBook). Add the line if missing; never overwrite the file, it also holds client ids and tokens; never commit it.
```bash
tools/gradle-serial.sh check # THE gate: build, unit tests, lint, architecture rules, screenshot tests
tools/gradle-serial.sh installDebug # install the debug app on the attached phone
# Android gate, from android/ (the script finds the repository root itself and runs android/gradlew):
../tools/gradle-serial.sh check # THE Android gate: build, unit tests, lint, architecture rules, screenshot tests
../tools/gradle-serial.sh installDebug # install the debug app on the attached phone
# Server gate, from server/:
cargo fmt --check && cargo clippy -- -D warnings && cargo test # THE server gate
# Either directory:
tools/install-git-hooks.sh # once per checkout
tools/clean-builds.sh [--dry-run] # delete build output in every checkout and worktree (disk)
python3 tools/dependency-report/latest.py # version drift of gradle/libs.versions.toml
tools/clean-builds.sh [--dry-run] # delete Gradle build output in every checkout and worktree (disk)
python3 tools/dependency-report/latest.py # version drift of android/gradle/libs.versions.toml
```
* **`check` is the single gate.** Every check a plan asks for must be reachable from it. Report gates with the real result lines (BUILD line, actionable tasks, test counts from fresh XML only). Never report a check as passing that you did not run in this session.
* **One Gradle build at a time.** Parallel builds exhaust memory. Always `tools/gradle-serial.sh`; it waits on a lock in `/tmp` and then runs the wrapper of your worktree.
* **`tools/gradle-serial.sh check` (Android) and `cargo fmt --check && cargo clippy -- -D warnings && cargo test` (server) are the only gates.** Every check a plan asks for must be reachable from one of them (the OpenAPI match test and the test-vector tests included). Report gates with the real result lines (BUILD line, actionable tasks, test counts from fresh XML only). Never report a check as passing that you did not run in this session.
* **One Gradle build at a time.** Parallel builds exhaust memory. Always `tools/gradle-serial.sh`; it waits on a lock in `/tmp` and then runs `android/gradlew` of your worktree.
* **Disk.** Three agents plus the main checkout can fill a disk. `~/.gradle/init.d/cache-retention.gradle.kts` (user-level, outside the repo) keeps `~/.gradle/caches` at 7 days with cleanup on every build; Gradle 9 accepts build-cache retention only there. Run `tools/clean-builds.sh` after removing a worktree or when free space is under about 5 GB. Deleting `~/.gradle/caches/build-cache-1` and old `~/.gradle/caches/<version>` folders is allowed; the next build is a slow full one. An agent that hits "no space left on device" loses only uncommitted work: free space and resume it, do not respawn.
* **Version drift:** run the dependency report at the start of every plan. A library more than one stable minor behind gets a line in the plan's report; a toolchain component more than one line behind triggers an upgrade plan first.
* **Screenshots.** Goldens are committed; `check` fails when a render differs. Re-record only for an intended change and **open every changed PNG** before committing. Code that compiles is not evidence of a good screen.
* **Every schema change gets a migration and a migration test.** User decisions stored in the database are not derived data and must survive every migration.
* **Every schema change gets a migration and a migration test** (sqlx migration on the server, Room migration on the phone). User data stored in the database is not derived data and must survive every migration.
* **Contracts are versioned.** A change to any contract file (`contracts/openapi.yaml`, the SQLite schema, the sync protocol, the Room entities, `contracts/test-vectors/*.json`, the push payload) requires a spec version bump and a `docs/decisions.md` entry in the same change. Number both from the brief.
* **Cargo builds** are not serialised by `gradle-serial.sh`; run at most one `cargo build`/`cargo test` per worktree at a time and never build Docker images for the Pi while a Gradle build runs.
---
@@ -99,12 +115,14 @@ python3 tools/dependency-report/latest.py # version drift of gradle/libs.versi
Enforced by an architecture check inside `check` where possible. Fill in from SPEC; the shape that worked:
* **Layering:** `feature → core:model`, `domain → core:model`, `data → core:model`; only `:app` sees everything. Pure modules (`:core:model`, `:domain:*`) import only `kotlin.*`, `kotlinx.*` and the project's own packages: no `java.time`, no `java.io`, no Android.
* **Layering (Android):** `feature → core:model, core:design`; `core:design → core:model`; `domain → core:model`; `data → core:model, domain`; `core:fakes → core:model, domain`; `adapter:fcm → core:model`. Only `:app` sees everything and binds the `:data` repositories to the `:core:model` interfaces through Hilt. Features never see `:data` or `:domain` (SPEC §6.2). Pure modules (`:core:model`, `:domain`) import only `kotlin.*`, `kotlinx.*` and the project's own packages: no `java.time`, no `java.io`, no Android.
* **Vendor SDKs live in one adapter module each** and map to the project's own records inside it. Sources are plugins behind a registry; nothing names a vendor outside its adapter. Source ids are open strings, never enums.
* **The UI contract is the contract:** features consume `*State` classes through repository interfaces; a contract change is made in the model, the fakes and the real repositories in the same change.
* **Fakes for every repository**, driven by scenarios, so screens render and screenshot-test without a device. Fakes never reach a release build.
* **Fast-moving APIs:** confirm current usage from the official documentation at implementation time. Do not rely on memory.
* **Licences first:** no vendor binary and no model file is committed, bundled or downloaded by a build before the owner has cleared its licence.
* **Server layering:** `kvitt-domain` has no I/O and depends only on `serde` (and `uuid` for ids); `kvitt-server` and `kvitt-import` depend on it, never the reverse. Money and split rules exist only in `kvitt-domain` and `:domain`, and both pass the same `contracts/test-vectors/*.json`.
* **No `unwrap()`/`expect()` in server non-test code** except in `main` start-up; no `unsafe`; errors map to the error codes of SPEC §14.3.
---
@@ -117,7 +135,7 @@ Enforced by an architecture check inside `check` where possible. Fill in from SP
* **No hard-coded colours, sizes or strings in feature modules.** Tokens from one design file; strings from resources, every supported language.
* **No placeholder text** in previews or screenshots.
* **No assumptions:** read a file before modifying or citing it.
* **Never commit:** `local.properties`, keystores, `.aar` or other vendor binaries, model files, client ids, tokens, personal data.
* **Never commit:** `local.properties`, keystores, `.aar` or other vendor binaries, model files, client ids, tokens, personal data, `google-services.json`, service-account keys, TLS keys, SQLite databases, backups, the Splitwise CSV, built APKs.
---
+6 -4
View File
@@ -5,11 +5,13 @@ What is here and why:
- `CLAUDE.md`: the working rules (orchestrator + Opus agents, worktrees, serial builds, gate, phone, owner steps). Fill in the marked blanks before the first plan.
- `docs/HANDOFF.md`, `docs/decisions.md`: templates for the one handoff and the one decisions log.
- `prompts/ORCHESTRATOR-prompt.md`: paste this to start an orchestrator session.
- `tools/gradle-serial.sh`: one Gradle build at a time across all worktrees (lock in /tmp/kvitt-gradle.lock).
- `tools/gradle-serial.sh`: one Gradle build at a time across all worktrees (lock in /tmp/kvitt-gradle.lock); runs the wrapper in `android/`.
- `tools/clean-builds.sh`: delete build output in every checkout and worktree; `--dry-run` lists.
- `tools/install-git-hooks.sh` + `tools/git-hooks/pre-commit`: secrets guard + fast tests; adjust the Gradle task.
- `tools/dependency-report/latest.py`: version drift of `gradle/libs.versions.toml` (needs that file).
- `gradle.properties`, `gradle/gradle-daemon-jvm.properties`: the JDK 25 pin and caching flags. Merge into the generated project's files.
- `tools/install-git-hooks.sh` + `tools/git-hooks/pre-commit`: secrets guard + fast tests (`:core:model:test` in `android/`,
`cargo test -p kvitt-domain` in `server/`), each skipped until its project exists.
- `tools/dependency-report/latest.py`: version drift of `android/gradle/libs.versions.toml` (needs that file).
- `android/gradle.properties`, `android/gradle/gradle-daemon-jvm.properties`: the JDK 25 pin and caching flags. The Gradle
project lives in `android/`; merge these into its generated files, do not move them back to the root.
- `.gitignore`: starter list of what never reaches git.
- `.claude/memory-seed/`: the agent memories generalised from Ebb. They are already installed in
`~/.claude/projects/-Users-jhag-projects-kvitt/memory/` (the folder Claude Code loads for `~/projects/kvitt`),
+2315
View File
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -3,17 +3,27 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at <hash>, last gate: <date, result line>.
- Installed on the owner's phone: <package> from <hash>.
- `main` at `8bef22a` (root commit "Scaffold: agent starter kit (seeded from Ebb)"). No gate yet: neither `android/` nor `server/` has a project.
- Branch `agent-spec` (on top of `main`, not landed): `SPEC.md` v0.1, filled `CLAUDE.md`, Gradle files moved to `android/`, tool scripts adjusted.
- Installed on the owner's phone: nothing.
- **Bootstrap exception (2026-10-17):** the repository had no commits, so the normal worktree flow could not start. The spec agent created the root commit directly in the main checkout (stray `.DS_Store` files deleted, `.DS_Store` added to `.gitignore`), then branched `agent-spec` there. From now on the normal flow of CLAUDE.md › Orchestration applies.
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-spec` (orchestrator).
- Plans agent: write `PLAN-*.md` and a start prompt per plan in `prompts/` from SPEC §11 (M1, M2a, M2b, M2c, M3, M4, M5, M6, M7a-M7f, M8, M9, M10, M11, U1, U2).
- UX agent: U1 UX directions (SPEC §13), in parallel with M1. Owner reviews the directions before U2 and before any UI work.
- Owner: answer SPEC §12 OQ-1 to OQ-4 (needed by M2c and M9).
## 3. Owner questions (asked, unanswered)
- OQ-1 Local container registry address, HTTP or HTTPS, credentials (SPEC §12).
- OQ-2 Splitwise CSV format confirmed against a real export, group list and target account names, non-SEK rows (SPEC §12).
- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; certificate directory on the HA Pi; which SSH add-on (SPEC §12).
- OQ-4 Google Drive folder id for backups (SPEC §12).
## 4. Paused (waiting on something outside the repo)
## 5. Numbering
Next free: D-001, SPEC v0.1, schema 1.
Next free: D-001 (no decisions logged yet; the interview decisions are in SPEC v0.1 itself). SPEC v0.1 is written; the next spec version is v0.2. Schema version 1 is defined in SPEC §14.1 (not yet implemented); the next schema version is 2. Sync protocol 1, push payload 1.
## 6. How the work is run
See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not
-1
View File
@@ -5,4 +5,3 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| # | Date | Decision | Why | Where it lives |
|---|---|---|---|---|
| D-001 | YYYY-MM-DD | … | … | SPEC §… |
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Version drift report for gradle/libs.versions.toml (Plan 09, D-056).
"""Version drift report for android/gradle/libs.versions.toml (Plan 09, D-056).
Reads every [versions] entry, finds an artifact that uses it in [libraries] or [plugins], and asks Google Maven and
Maven Central for that artifact's maven-metadata.xml. Prints one table: current, latest stable, newest (only where a
@@ -19,7 +19,7 @@ import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
ROOT = Path(__file__).resolve().parents[2] / "android" # the Gradle project lives in android/
REPOS = ["https://dl.google.com/android/maven2", "https://repo1.maven.org/maven2"]
# Catalog entries that are SDK levels, not artifacts.
NOT_ARTIFACTS = {"minSdk", "compileSdk", "targetSdk"}
+23 -16
View File
@@ -1,10 +1,12 @@
#!/bin/sh
# Kvitt pre-commit hook: the fast subset of `./gradlew check`. Installed by tools/install-git-hooks.sh.
# Kvitt pre-commit hook: the fast subset of both gates. Installed by tools/install-git-hooks.sh.
# Each check runs only when its project exists, so the hook works on branches before android/ or server/ exist.
set -e
top=$(git rev-parse --show-toplevel)
# Secrets and personal data never reach git. Adjust the list to this project (see CLAUDE.md › Never commit).
bad=$(git -C "$top" ls-files --cached -- local.properties '*.jks' '*.keystore' '*.p12' '*.aar' 'models/' 'export/' | head -5)
# Secrets and personal data never reach git (CLAUDE.md › Never commit).
bad=$(git -C "$top" ls-files --cached -- '*local.properties' '*.jks' '*.keystore' '*.p12' '*.aar' 'models/' 'export/' \
'*service-account*.json' '*google-services.json' '*.pem' '*.sqlite' '*.db' 'infra/secrets/' | head -5)
if [ -n "$bad" ]; then
echo "pre-commit: a secret, keystore, vendor binary or personal-data file is staged or tracked:" >&2
echo "$bad" >&2
@@ -12,17 +14,22 @@ if [ -n "$bad" ]; then
exit 1
fi
[ -x "$top/gradlew" ] || exit 0
# Android: the pure model tests, on the pinned JDK (android/gradle.properties); the shell's java may be a stub.
if [ -x "$top/android/gradlew" ] && [ -d "$top/android/core/model" ]; then
case "$("${JAVA_HOME:+$JAVA_HOME/bin/}java" -version 2>&1 | head -1)" in
*\"25*) ;;
*) [ -d /opt/homebrew/opt/openjdk@25 ] && export JAVA_HOME=/opt/homebrew/opt/openjdk@25/libexec/openjdk.jdk/Contents/Home ;;
esac
(cd "$top/android" && ./gradlew --quiet :core:model:test) || {
echo "pre-commit: Android fast tests failed (see above). Run tools/gradle-serial.sh check for the full picture." >&2
exit 1
}
fi
# The build runs on the pinned JDK (gradle.properties); the shell's default java may be newer or a macOS stub.
case "$("${JAVA_HOME:+$JAVA_HOME/bin/}java" -version 2>&1 | head -1)" in
*\"25*) ;;
*) [ -d /opt/homebrew/opt/openjdk@25 ] && export JAVA_HOME=/opt/homebrew/opt/openjdk@25/libexec/openjdk.jdk/Contents/Home ;;
esac
cd "$top"
# Replace with this project's fast checks (architecture rules, the pure-module tests). Keep it under a minute.
./gradlew --quiet :core:model:test || {
echo "pre-commit: fast tests failed (see above). Run ./gradlew check for the full picture." >&2
exit 1
}
# Server: the pure domain crate tests.
if [ -f "$top/server/Cargo.toml" ] && [ -d "$top/server/crates/kvitt-domain" ] && command -v cargo >/dev/null 2>&1; then
(cd "$top/server" && cargo test --quiet -p kvitt-domain) || {
echo "pre-commit: kvitt-domain tests failed (see above). Run the server gate for the full picture." >&2
exit 1
}
fi
+4 -2
View File
@@ -1,9 +1,11 @@
#!/bin/sh
# Runs ./gradlew with the given arguments, one Gradle build at a time across every worktree and agent on this machine
# (CLAUDE.md › Build and verify: parallel builds exhaust memory). Waits for a running build to finish, then runs.
# Usage from any worktree root: tools/gradle-serial.sh check
# The Gradle project lives in android/; the wrapper there is used whatever directory this is started from.
# Usage from any directory of a checkout or worktree: tools/gradle-serial.sh check (or ../tools/gradle-serial.sh from android/)
lock=/tmp/kvitt-gradle.lock
top=$(git rev-parse --show-toplevel) || exit 1
[ -x "$top/android/gradlew" ] || { echo "gradle-serial: no android/gradlew in $top yet" >&2; exit 1; }
while ! mkdir "$lock" 2>/dev/null; do
pid=$(cat "$lock/pid" 2>/dev/null)
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then rm -rf "$lock"; continue; fi
@@ -11,4 +13,4 @@ while ! mkdir "$lock" 2>/dev/null; do
done
echo $$ > "$lock/pid"
trap 'rm -rf "$lock"' EXIT INT TERM HUP
cd "$top" && ./gradlew "$@"
cd "$top/android" && ./gradlew "$@"
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Installs the Kvitt pre-commit hook into this repository's hooks folder (shared by all worktrees).
# The hook guards itself: on branches without the app build it only runs the export/ check.
# The hook guards itself: on branches without android/ or server/ it only runs the secrets check.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
hooks="$(git -C "$here" rev-parse --path-format=absolute --git-common-dir)/hooks"