feat(hooks): commit gate runs detected gates with content-bound evidence (U5b — PR4) - #48
Merged
Merged
Conversation
…nce (U5b)
pre-commit-verification.sh moves from advisory-only to enforcing (jq
present; jq-absent path unchanged, still silent fail-open). On `git
commit` it now runs every gate gate-lib.sh detects, each under
timeout "${CLAUDE_GATE_TIMEOUT_SECS:-120}" from the project dir,
logging to .claude/hooks/.state/gate-<label>.log:
- All green -> writes a hook-authored evidence stamp
(.claude/hooks/.state/commit-verified, `{epoch, tree-hash}`),
trusted on a later commit only when BOTH <=5 minutes old AND its
tree-hash matches the current `git write-tree` output -- content-
bound, not time-only, so an edit staged seconds ago forces a
re-run despite an otherwise-fresh stamp.
- A red gate -> denies, naming the gate, its log, and the existing
anti-test-deletion sentence.
- A gate exceeding its budget -> asks (never a silent kill or hang);
no stamp is written.
- No gates detected -> falls back to the original advisory text,
unchanged (its self-write instruction is removed since the stamp
is hook-authored only in every branch now).
- CLAUDE_SKIP_GATE_HOOK=1 escape hatch allows unconditionally, with
the skip always disclosed in context.
settings.json: raises only this hook's registered timeout 5 -> 300
(per-gate defaults sum safely under it). docs/hooks.md gains a
"Quality Gates" subsection; MIGRATION.md and CHANGELOG.md record the
newly-blocking behavior.
New scripts/hook-tests.d/30-gates-lane.sh covers red/green/timeout/
escape-hatch/cache-invalidation against three fixtures. failing-
project and slow-gate each gain an empty package-lock.json (verified
via gate_lib_detect: no lockfile picks pnpm, which ubuntu-latest
runners don't ship, so an npm-selecting lockfile keeps the fixtures
CI-executable); new sibling fixture passing-project covers the
green/stamp path. slow-gate also gains a package.json wiring its
"test" script to `bash gate.sh`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lback) Stock macOS ships no GNU timeout — unguarded, every gate run would exit 127 and deny all commits. Resolve timeout|gtimeout once; degrade to unbounded-within-hook-ceiling when neither exists, documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
U5b (PR4, gates lane 2 of 3):
pre-commit-verification.shgoes from advisory-only to enforcing — it runs the project's detected quality gates itself and blocks red commits with hook-authored, content-bound evidence. This is the plan's centerpiece behavior change (Decision D1's commit-side half) and closes the audit's flagship finding: the old hook never ran a test and trusted a stamp the agent wrote itself.What changed
git commit(jq present): a cached stamp is trusted only if ≤5 min old and its recordedgit write-treehash matches the current index — a post-green edit forces a re-run (no stale-evidence rides, no cross-session stamp reuse). Otherwise gates run viagate-lib.shper stack, each under its own timeout, logging to.state/gate-*.log.{epoch, tree-hash}stamp (grep-verified: nothing instructs the agent to write it).CLAUDE_SKIP_GATE_HOOK=1escape hatch, disclosed when used. No detectable gates → the original advisory text, unchanged (this repo itself exercises that path).settings.json: this hook's timeout 5 → 300 (the review found gates could never fit the old 5s registration).timeoutresolved as timeout → gtimeout → unbounded-within-hook-ceiling; stock macOS has no GNU timeout, and unguarded it would have exit-127'd every gate and denied all commits.package-lock.json→ npm detection; newpassing-project); 18 new harness cases in30-gates-lane.sh(red-deny, green-stamp, tree-hash-mismatch re-run, slow-gate ask, escape hatch); MIGRATION.md "commits are newly blocking" entry.Provenance
test-hooks.shALL GREEN (25 cases incl. the 18 new);check-invariants.shALL GREEN; CI-equivalent shellcheck clean.Risk tier
high — this makes commits blocking wherever a stack is detected. Mitigations per Decision D1: escape hatch, timeout→ask (never silent kill), jq-absent stays advisory (announced at session start), no-gates path unchanged, MIGRATION entry with the disable path, and the portable-timeout fallback so no platform is bricked.
Test plan
CLAUDE_GATE_TIMEOUT_SECSknob)🤖 Generated with Claude Code