docs: a 2 is a question, not automatically a failure

TOOLS.md said "exit code 2 is never a pass", which is right and was only half
the rule. The pre-commit hook read it as "2 is always a refusal" and made every
deletion-only commit impossible while reporting a credential that did not exist.

Both documents now carry the completed rule: on a 2, ask whether the check could
have had anything to look at. The hooks README records the one place the two
scripts are treated differently and why.
This commit is contained in:
null 2026-08-18 15:00:49 -05:00
parent d7fe58883c
commit 418261b482
2 changed files with 21 additions and 0 deletions

View File

@ -69,6 +69,21 @@ the outside and only one of them is evidence. A hook or a CI step that treats a
`2` as success has quietly turned the check off. Each script states its codes at `2` as success has quietly turned the check off. Each script states its codes at
the top; read them there. the top; read them there.
**And a `2` is not automatically a failure either — it is a question.** The
pre-commit hook learned this the hard way: `secrets.sh` exits `2` on a commit
that only *deletes* files, because a deletion has no added lines to scan, and
that is the correct answer. The hook treated every non-zero as a refusal and
printed *"possible credential in the staged changes"* over a plain `git rm`, so
deletion-only commits were impossible and the reason given was a security
problem that did not exist.
The rule that came out of it: **on a `2`, ask whether the check could have had
anything to look at.** The hook now refuses a `2` whenever the staged diff adds
lines — because then the scanner did have input and checking nothing is exactly
the failure `2` reports — and accepts it, saying so, when the diff only removes
content. Both directions are proved with `prove-guard.sh`-style injections
before either is believed.
The hooks are the other place work gets stopped: The hooks are the other place work gets stopped:
[`architecture/githooks/README.md`](architecture/githooks/README.md) has the one [`architecture/githooks/README.md`](architecture/githooks/README.md) has the one
install command and the table of what each hook runs. Note that **`post-commit` install command and the table of what each hook runs. Note that **`post-commit`

View File

@ -68,6 +68,12 @@ exactly the failure it claimed to catch.
It exits `2` for "nothing was checked", and the hook treats `2` as a refusal. It exits `2` for "nothing was checked", and the hook treats `2` as a refusal.
A missing schema directory is not a clean schema. 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 **`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 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 and invisible — but it is a surprise if you were not expecting it. It never