# Git hooks ``` Status: Current Owner: _null Last reviewed: 2026-08-18 Governs: .githooks/ — what runs before and after a commit Review trigger: A new guard the repository wants run before a commit; any change to what a commit message must contain; any change to which Gradle tasks pre-commit runs ``` Three hooks, and the reason they live in the repository rather than in `.git/hooks`: that directory is not versioned, so a hook living there protects exactly one clone on exactly one machine. ## The hooks are in `.githooks/`, and only there The template this repository adopted keeps a master copy under `docs/architecture/githooks/` and installs copies into `.githooks/`. **Period does not**, deliberately: `pre-commit` here is adapted for Gradle rather than npm, so a second copy would be a second version of a file somebody edits once and forgets — the exact failure `../../DOC_TRUST_MAP.md` exists to prevent. So [`.githooks/`](../../../.githooks) holds the hooks and this document describes them. One copy of the code, one copy of the explanation. ## Install ```bash git config core.hooksPath .githooks ``` That is the whole setup, and it is **per clone** — every checkout runs it once, including a fresh clone on the same machine. An uninstalled hook fails silently, which is the same class of problem the hooks exist to prevent. ## What each one does | Hook | Guard | | --- | --- | | `pre-commit` | the staged-diff secret scan; then `:domain:cycle:test` and `:domain:prediction:test` when `.kt`/`.kts` or a build file is staged; then `scripts/schema-guard.sh` when a Room entity or the schema export is staged | | `commit-msg` | refuses a message with no conventional type — the closed vocabulary is in the hook's own header | | `post-commit` | pushes to `origin`, so a guarded commit does not sit unpushed | ## Two things worth knowing before you rely on them **`pre-commit` does not compile the Android modules.** It runs the two pure-JVM suites, which need no SDK and take about a second. Compiling `:app` needs the Android SDK and half a minute, and a hook people reach for `--no-verify` to avoid is worse than one that checks less. `./gradlew assembleRelease` belongs to [`../../security/SECURITY_CHECKLIST.md`](../../security/SECURITY_CHECKLIST.md), which is where it is. The suite it *does* run is not an arbitrary subset: it is the prediction acceptance cases from [`../../planning/PRODUCT_PLAN.md` §51](../../planning/PRODUCT_PLAN.md), which guard the one claim this product is built on. **The schema guard is here rather than in the suite for a reason worth knowing.** Room rewrites the schema export during compilation, so by the time any test runs, both sides of any in-process comparison describe the changed entity and agree. Only git can see that an already-committed schema file changed, and only a hook can ask git before the commit exists. `scripts/schema-guard.sh` carries the proof in its header — the check it replaced was watched staying green over exactly the failure it claimed to catch. It exits `2` for "nothing was checked", and the hook treats `2` as a refusal. A missing schema directory is not a clean schema. **The secret scan's `2` is handled differently, and deliberately.** A commit that only deletes files gives it nothing to scan, which is the right answer rather than a fault — so the hook accepts a `2` when the staged diff adds no lines, and refuses it whenever the diff adds any. Before that distinction existed, `git rm` was impossible and the refusal claimed a credential had been found. **`post-commit` pushes.** That is the intent — the commit that first added a pre-commit hook to the project this came from sat unpushed for a day, guarded and invisible — but it is a surprise if you were not expecting it. It never forces, stays out of the way mid-rebase, and `SKIP_PUSH=1` opts out loudly. It has a second consequence worth knowing about: the push is what the Command Center reads, so **whatever documentation was not in that commit is now behind the code by one push**. That is the mechanical reason [`../../WORK_CYCLE.md`](../../WORK_CYCLE.md) asks for doc edits in the same commit as the change rather than in a tidy-up afterwards — with this hook installed, "I will document it next commit" means the site has already published the version without it. ## Escape hatches, and why they are loud ```bash SKIP_GUARDS=1 git commit ... # skips the scan and the tests, and says so SKIP_PUSH=1 git commit ... # commits without publishing, and says so git commit --no-verify ... # skips the hooks entirely, silently ``` Prefer the first two. They leave a line in the terminal saying the guard did not run, which is the difference between a deliberate exception and a habit. ## Why a hook and not CI Both, eventually. These are the guards that must run before the artifact exists: a check that fires after a push, or after a Play upload, catches the problem once it is already somewhere it cannot be taken back from. CI is the second opinion; this is the one that runs first.