Skip to content

feat(hooks): commit gate runs detected gates with content-bound evidence (U5b — PR4) - #48

Merged
dralgorhythm merged 2 commits into
mainfrom
claude/hardening-pr4-gates-enforce
Jul 24, 2026
Merged

feat(hooks): commit gate runs detected gates with content-bound evidence (U5b — PR4)#48
dralgorhythm merged 2 commits into
mainfrom
claude/hardening-pr4-gates-enforce

Conversation

@dralgorhythm

Copy link
Copy Markdown
Owner

Summary

U5b (PR4, gates lane 2 of 3): pre-commit-verification.sh goes 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

  • On git commit (jq present): a cached stamp is trusted only if ≤5 min old and its recorded git write-tree hash matches the current index — a post-green edit forces a re-run (no stale-evidence rides, no cross-session stamp reuse). Otherwise gates run via gate-lib.sh per stack, each under its own timeout, logging to .state/gate-*.log.
  • Red gate → deny naming exactly one gate + its log path + the anti-test-deletion sentence. Timeout → ask, honestly worded, no stamp. All green → hook writes the {epoch, tree-hash} stamp (grep-verified: nothing instructs the agent to write it). CLAUDE_SKIP_GATE_HOOK=1 escape 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).
  • Portability fix (orchestrator follow-through): timeout resolved 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.
  • Fixtures made CI-executable (empty package-lock.json → npm detection; new passing-project); 18 new harness cases in 30-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

  • Author: worker-builder (sonnet) via /swarm-execute; rebased over the merged hooks lane by the orchestrator (MIGRATION/docs keep-both resolutions); independent gate re-run on the lane tip.
  • Gates: test-hooks.sh ALL GREEN (25 cases incl. the 18 new); check-invariants.sh ALL 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

  • Failing fixture denies naming the gate + log + test-deletion warning; green fixture stamps; staged edit after green forces re-run (tree-hash mismatch)
  • Slow gate → ask with no stamp (CLAUDE_GATE_TIMEOUT_SECS knob)
  • Escape hatch allows with disclosure; repo-root (no gates) advisory case unchanged
  • macOS-portability: timeout/gtimeout/unbounded fallback paths

🤖 Generated with Claude Code

dralgorhythm and others added 2 commits July 24, 2026 11:53
…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>
@dralgorhythm
dralgorhythm merged commit 00584f5 into main Jul 24, 2026
5 checks passed
@dralgorhythm
dralgorhythm deleted the claude/hardening-pr4-gates-enforce branch July 24, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant