# 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/`. **This
repository 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, the tests and the message check, 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.
