Repository navigation
fix(hooks): a commit the gate cannot see is a commit it does not gate - #353
Conversation
…must see it `.claude/hooks/pre-commit-gate.sh` stopped gating real commits. #324 gave it a self-filter - no `git commit` in the payload means exit 0 - after the hook was seen blocking read-only commands with a red gate's own error. The filter is right; what feeds it was not. `commit_bypass_counts` reopened command position only on OPERATORS (`&&`, `;`, `|`, parens), so the shell RESERVED WORDS that introduce a command - `then`, `do`, `{`, `!`, `time` - consumed it instead, and every commit behind one counted as zero: if ok; then git commit -m x; fi gate did not run for f in a b; do git commit -m "$f" gate did not run git status && { git commit -m x; } gate did not run ! git commit -m x gate did not run That miscount was harmless while zero commits meant "run the gate anyway" - the shapes above were gated by accident, for the wrong reason. The self-filter inverted the meaning of zero, so the same miscount became a silent exemption. A gate that has stopped firing is indistinguishable from a gate with nothing to say, which is the failure mode this whole directory exists to prevent. The parser now keeps command position across a reserved word, and only when the word is ITSELF in command position - so `echo do git commit` stays an echo. Nothing else moves: `--no-verify` is still the bypass and now reaches inside those constructs too, the per-commit counting that stops a later bypass exempting an earlier commit is untouched, and a payload with no commit in it still exits 0. Seven cases in `bin/test-check-agent-hooks.sh`, four of them red against the unfixed hook. The three that stay green are the near-miss the harness doc asks for: a compound non-commit command with `for`, `if` and `$(...)`, a keyword in argument position, and a bypass inside a construct - each one `git` away from being gated. Worth recording, since the script-side guard exists because the registration cannot be trusted: on Claude Code 2.1.231 the hook's `if: "Bash(git commit *)"` filters a plain `echo` correctly but does NOT filter a compound command - `for f in $(echo a b); do if [ -n "$f" ]; then echo "$f"; fi; done` reached an always-deny hook and was blocked. Verified with a scratch project, both directions. The prefix matcher is a belt; the script must hold its own trousers up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
✅ Duplicate Code ReportTwo engines run in parallel for cross-validation. Each has its own thresholds tuned to its baseline - the real safety net is the per-engine "max increase vs base" check. ✅ PMD CPD
No new clones introduced by this PR. ✅ jscpd (language-agnostic)
No new clones introduced by this PR. Powered by astubbs/duplicate-code-cross-check |
|
…d one that never was The same-defect sweep for the parser fix in this branch had to establish what the sibling hooks actually do, and two of this file's statements did not survive being run. `check-squash-subject.sh` is described as matching its merge in COMMAND POSITION, so that an echo naming one is text. It does not - it is a regex over the raw command, and the echo is denied. The near-miss case in `bin/test-check-agent-hooks.sh` that reads like a command-position test passes for an unrelated reason: its quoting makes shlex raise inside the matched slice. Left as it behaves, because a guard that can only ever REFUSE a merge is safe erring towards refusing - but the file must not promise a property the code does not have, or the next fix is built on it. Recorded under Known gaps with the reason it stays. The `if` known-gap said the registration matches the command as written and named only what it therefore MISSES. Verified against 2.1.231, it also fires on things it should not: a compound command - a `for` loop with a nested `if` and a command substitution - reached an always-deny hook and was blocked, while a plain echo was correctly filtered. That is the live misfire behind the parser fix on this branch, and it is why the belt-and-braces hook cannot lean on its own matcher. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
🧪🔒 Quarantine Lane Report
🔴 expected while the owner PR is open · 🟡🎲 flapper, pass proves nothing · 🚨 a deterministic quarantined test passing means its fix landed: delete its |
Takes #341's BSD-portability work (ffba759 and its four siblings). No conflicts, and nothing to reconcile: the overlap is real but disjoint within the file. `bin/test-check-agent-hooks.sh` is the one file both sides changed. Master replaced GNU-only constructs in the fixture helpers - `touch -d '@...'` could not age its own test data on BSD, which was hiding four of the other five defects that commit fixes. This branch appends seven cases to the `pre-commit-gate.sh` block. Different regions, so git merged them cleanly; verified rather than assumed, because a clean merge is not evidence here (AGENTS.md, "A clean merge is not evidence you were safe to skip it"): the merged file diffs against origin/master by exactly this branch's seven added cases and nothing else. `.claude/hooks/pre-commit-gate.sh` and `docs/agent-harness.md` - this branch's actual fix - are untouched by master. Master's hook edits are to three siblings (`check-merge-outstanding-work.sh`, `inject-recorded-knowledge.sh`, `remind-inflight-on-push.sh`). Those siblings are worth a note against this branch's own claim. Its PR body records that none of them track command position, only `pre-commit-gate.sh` does, and that this is the right posture because they can only ever REFUSE. Master's commit sharpens that: the merge guard was FAILING OPEN on macOS by reading an unreadable mtime as "nothing running", and now fails closed. The claim still holds - it is about parsing, not clocks - and the two fixes point the same way. Post-merge: `bin/test-check-agent-hooks.sh` 166 pass / 0 fail (master's 159 plus this branch's 7), `bin/check-issue-refs.sh` clean, `.githooks/pre-commit` green, and the four gated compound shapes re-probed by hand against a stub gate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
|
@claude review this |
|
Claude finished @astubbs's task in 8m 52s —— View job Review:
|
|
@claude review this |
|
Claude finished @astubbs's task in 1m 38s —— View job Review:
|
…oss it Review on this PR (#353) hand-traced two misses in the same defect class the PR names, both in commit_bypass_counts(): - An unquoted newline was swallowed by shlex's default whitespace, so it produced no token and never reopened command position the way `;` does. `git add -A<newline>git commit -m wip` - two ordinary sequential lines - counted zero commits, and zero commits means "skip the gate". Every existing test joined statements with `;`, so the suite never noticed. Fix: `\n` moves from `whitespace` to `punctuation_chars`, and a separator predicate recognises the glommed runs shlex produces (a bare "\n", ";\n\n", "&&\n") that exact OPERATORS membership cannot enumerate. The commit-extent scan stops at newlines too, or a later line's --no-verify would have exempted an earlier commit that never asked. - `function deploy { git commit -m x; }` was invisible: the function NAME consumed command position before the `{` introducer could reopen it. The `foo() { ... }` spelling was only ever caught because its bare `()` are operator tokens. Fix: skip the keyword and the name, leaving position open for the brace. Quoted newlines are unaffected - quoting resolves before either classification applies - and the backslash-line-continuation shape lexes identically before and after (verified), so the escape hatch loses nothing. Seven new cases in bin/test-check-agent-hooks.sh; the five gating ones verified red (exit 0, commit missed) against the old hook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
Both fixed in 9cb1cb1, and the manual traces verified by live repro first (the review job's sandbox couldn't run Python; local runs confirmed both:
Seven new cases in The environment note (a stray working-tree edit reverting this PR's fix in the review job's checkout) did not reproduce here — this branch's worktree was clean; likely runner-workspace residue. |
…no Performance overlap ChaosRevokeUnderWorkDrainIT.revokeUnderDrainingStopsStaysProtocolHonest went red on this PR's post-merge head c1f423e with the familiar signature: 4 CLASS2_STALL/LAG_STAGNATION, offsets stagnant 154s against the 150s bound, group STABLE - peak lagStagnation 154263ms, inside the 300ms band the twelfth sighting measured. Seed 6037000644302969438 captured before the log expires. What the entry adds to the ledger: this branch changes no Java (hooks, a test script, docs), so it is another not-PR-introduced control - and Performance finished at 04:14:18Z while the failing arm started 04:17:05Z, making it the first drain-arm red where runner contention is not available as the hypothesis. Recorded as unresolved; the seed is unreplayed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
This review raced with the fix push: both findings were closed in 9cb1cb1 (pushed 14:47Z, while this review job was reading |
Conflict in docs/inflight/bug-857-family.md: this branch and master both appended a chaos sighting numbered fourteenth. Master's landed first (#347's three reds, then the fifteenth from 07cb810), so this branch's entry - run 32807910210, seed 6037000644302969438 - takes the next number, sixteenth, per the convention the fifteenth entry itself states: numbers are recording order, and renumbering a merged entry breaks its citations. One claim in the entry was corrected during the resolution: it called itself the first drain-arm red with no Performance overlap, but master's fourteenth had already ruled out contention on timing for a drain red the same morning - so it is recorded as corroboration instead. Negative controls re-run after the merge: bin/test-check-agent-hooks.sh, 173 ok, 0 failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
|
Claude finished @astubbs's task in 3m 15s —— View job Review:
|
… PR merges bin/check-branch-self-reference.sh flagged the entry's two mentions of its own PR: a merged branch stops existing, so a doc that says "#353 changes only three files" would read as a live claim about a closed PR - and it had already gone stale, since this branch now also edits the ledger itself. Scoped the claim to the observed head c1f423e and marked both passages post-merge: checked, the same treatment the fourteenth and fifteenth entries carry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
That run is green on this head: the |
…d no-overlap datapoint Run 32863352692 drew both revoke-under-work arms at once - eager seed 5501517460666962649 and drain seed 7370431147468591204, 47 CLASS2_STALL violations, every peak inside the ~154s band. Recorded as an addendum to the sixteenth sighting per the ledger's own convention (a repeat signature keeps its seeds, not a new number). Performance again provably did not overlap the failing arms, which is the third same-day timing rule-out of the contention hypothesis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
…htness roster Integration Tests went red on a branch with no Java: PartitionStateCommittedOffsetIT.committedOffsetRemoved[2] earliest failed checkHowManyRecordsWithKeyPresent expecting 2 records and finding 1 - the same assertion signature the solved [1] latest nudge race carried (docs/plans/2026-08-05-001-investigate-committedoffset-latest-reflake.md), now on the earliest parameter. The ambient probe autopsy reads probe clean, pointing at the test rather than consumer-group progress, which matches the family signature. First recorded sighting of this parameter; recorded before rerunning so the flake is in the ledger rather than retried into silence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
The previous commit's self-reference gate failure was masked by piping the gate into tail -1 - the exact pipeline trap AGENTS.md documents - so the row citing this PR went up unconfirmed. The row already reads correctly post-merge (dated provenance, like the #204 row beside it); this adds the inline marker the gate requires, placed inside the cell so the table does not split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
Brings in the static-analysis lanes (#356 work): ShellCheck gate, -Xlint:all, SpotBugs-over-tests, the RacerD/PIT lane fixes, and the two pre-review commands bin/check-all.sh and bin/check-pr-analysis-surfaces.sh. docs/agent-harness.md merged cleanly on both sides' edits. Gates re-run after the merge per the inherited instruction: bin/check-all.sh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
The handoff said a babysitter subagent 'may still be running' for the pre-commit-hook fix. It has finished: head c73892e is CLEAN, every required check green including the human LGTM, zero open threads. Merge was deliberately not taken - it is the owner's call - so the next session needs to know the state rather than re-derive it, and that the worktree it held is now free. Also carries forward the two environment notes the run surfaced: the review runner's workspace holds an uncommitted revert of this very fix, and shellcheck is not installed locally so check-shell-lint.sh is CI-only here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
Second base movement while this PR sat open. Eleven commits, one conflict, and it is the same ledger file as last time. docs/inflight/bug-857-family.md - both sides appended. This branch added the SIXTEENTH sighting; master added the section that CLOSES the CLASS2_STALL line of that file, having finally run the discriminator the twelfth sighting pre-declared. Master's section wins the ordering - it supersedes every entry above it, including ours - so the sighting stays where the numbering puts it and the closing section stays last. Master's counts are left exactly as master wrote them. "Fourteen sightings" was written when the file held fifteen, so the number counts CLASS2_STALL crossings rather than sightings, and re-deriving it from this side would be inventing a fact about somebody else's measurement. A marked note records that the sixteenth arrived after that section was written and is one more of the same crossings, not an exception to them. Verified on the merged tree: bin/test-check-agent-hooks.sh 173/173 - this PR's own subject - and bin/check-all.sh 12/12 with only check-shell-lint CANNOT (shellcheck is not installed on this box; it passes in CI). The clone was shallow again, for the second time today, and `git merge-base` was answering exit 1 until it was unshallowed - the hazard master's new check-shallow-history.sh hook exists for, observed live while merging the PR that ships beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HwDcZdfv1Ne4u3g6ThrG9
…-fires-on-noncommit
then or do is a commit, and the gate must see itNo textual conflict, which is the case this repo has learned not to trust: the three files both sides touch - AGENTS.md, bin/test-check-agent-hooks.sh and docs/agent-harness.md - auto-merged, so each was audited in BOTH directions (`git diff origin/master -- <file>` and `git diff HEAD -- <file>`) to confirm neither side's intent was dropped. bin/test-check-agent-hooks.sh - master's #353 added 14 assertions to the pre-commit-gate section (keyword command position, multi-line payloads, the `function` spelling); this branch's additions are the inject-branch-context.sh section at the end and the private-TMPDIR isolation in the push-reminder section. Disjoint regions, both present. Verified by name: all 14 of master's new assertions are in the merged file, and the merge-outstanding arm still builds its scratch repository rather than writing docs/inflight/pr-90001-selftest.md into the real working tree. docs/agent-harness.md - master rewrote the `check-squash-subject.sh` bullet and added two Known-gaps entries about `if` filtering; this branch added the `inject-branch-context.sh` section and the settings.json count. Both present. This branch's claim that the hook self-filters "the same shape check-squash-subject.sh uses" still holds after master's edit: that edit corrects how the squash guard finds a merge, not how it is registered. AGENTS.md - master appended the never-put-prose-in-a-shell-string rule under Commits; this branch rewrote the inherit-rule heading and added its second trigger. Different sections, both present. Deliberately NOT taken from master: nothing. Deliberately preserved against reintroduction: the removed BSD `mktemp` arm stays removed - the note at bin/test-check-agent-hooks.sh "NO BSD `mktemp` ARM HERE, DELIBERATELY" survives and no stub came back with master. Reseal assertions re-checked on the merged tree, because master could have touched either file and did not: zero occurrences of "Added after the fact" in docs/plans/2026-08-25-002-test-jcstress-poc-plain-long-visibility.md, and exactly one "A rate is a claim about one machine" section in docs/solutions/best-practices/a-stress-probe-is-an-instrument-you-built-not-a-test.md. Verification on the merged tree: bin/test-check-agent-hooks.sh 225 ok / 0 fail serially, and 225 ok / 0 fail in each of four concurrent runs. The 211 baseline plus master's 14 new arms. bin/check-all.sh 13 passed, 0 failed, 1 could not run (check-shell-lint.sh - no local ShellCheck) bin/check-all.sh --tests-only 24 passed, 0 failed, same single cannot-run shell lint run through the cached koalaman/shellcheck image with the lane's own command over the 76 tracked scripts: clean at severity error, and bin/test-check-shell-lint.sh 7/7 with that same binary on PATH branch self-reference PR_NUMBER=350 GITHUB_HEAD_REF=... - green with the PR resolved, not the bare false green bash parse sweep 79 scripts, 0 failures - but under bash 5.2. Master's new `shell: macos` lane is bash 3.2, and this branch's shell has never met it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven commits, merge-base f7e58d7. One textual conflict, plus two things master brought down that change what this branch can claim. CONFLICT: docs/agent-harness.md, in the settings.json hook list. Both sides rewrote the same bullet's tail. Master's #353 corrected a false claim about check-squash-subject.sh - the file said the hook matches `gh` in COMMAND POSITION, and running it showed it matches a regex over the raw command, so an `echo` naming a subject-overriding merge is denied. This branch had appended its own bullet for warn-low-disk.sh immediately after that tail. Resolved by taking BOTH: master's corrected paragraph replaces the wrong claim, and this branch's warn-low-disk.sh bullet follows it unchanged. Taking either side alone would have dropped the other's whole contribution. bin/test-check-agent-hooks.sh auto-merged, and the auto-merge is correct: master added 35 cases to the pre-commit-gate section (keywords, multi-line payloads, the `function` spelling), this branch added its cases further down, and the private-TMPDIR fix in the push-reminder section is untouched. Verified by running it rather than by reading the diff. The hook-count assertion did not fire. #353 registered no new hook, so settings.json still holds nine and the prose still says nine. WHAT MASTER CHANGED ABOUT THIS BRANCH'S CLAIMS 11c6551 added a macOS lane that runs every bin/test-*.sh under BSD userland and bash 3.2. This branch's deliverable is a shell hook with `stat -f`/`stat -c` platform branching whose BSD arm the inflight note records as never having run, so that gap now has a CI answer for the first time. Recorded in the note. 3a98b8d/957d08423 added a shared-git-state hazard row and taught check-shell-hazards.sh to join backslash continuations and to see wrapper command words. The two `hazard-ok:` declarations on warn-low-disk.sh's platform `case` still satisfy it - the gate passes. VERIFICATION ON THE MERGED TREE bin/check-all.sh --with-tests 39 ran, 37 passed, 0 failed, 2 could not run (check-shell-lint.sh and its self-test: ShellCheck is not installed here) shellcheck run through the cached koalaman/shellcheck image with the lane's own command over all 76 tracked scripts: clean at the `error` floor, and clean at `warning` for this branch's files check-branch-self-reference.sh green with PR_NUMBER=339 and GITHUB_HEAD_REF=ci/warn-low-disk-hook, which is the only way it checks anything hook suite, serial 208 ok, 0 failures (master alone: 173 ok, 0 failures - the same 35-case delta as before) CONCURRENCY, MEASURED AGAINST A PRISTINE-MASTER CONTROL Four concurrent copies, this branch: 3/4 red, all on one arm. Four concurrent copies of a detached origin/master worktree: 4/4 red, on that same arm AND on five push-reminder assertions. So the private-TMPDIR fix survived the merge and still works - zero push-reminder failures on this branch against five on master. The arm that remains red is check-merge-outstanding-work.sh's note fixture, which writes docs/inflight/pr-90001-selftest.md into the real working tree at a fixed path; it is pre-existing on master, untouched by this branch, and already fixed on #350, which this branch's own inflight note and PR body both name as its owner. Deliberately not fixed here: two branches editing those lines is the conflict the note exists to prevent.
Description
.claude/hooks/pre-commit-gate.shhad stopped gating real commits, in a way nothing would report.#324 gave the hook a self-filter - no
git commitin the payload meansexit 0 - after it was seen blocking read-only commands with a red gate's own error. That filter is
correct and stays. What feeds it was not:
commit_bypass_countsreopened command position only onoperators (
&&,;,|, parens), so the shell reserved words that introduce a command consumed itinstead, and every commit behind one counted as zero.
While zero commits meant "run the gate anyway", those shapes were gated by accident - right outcome,
wrong reason. The self-filter inverted the meaning of zero, and the same miscount became a silent
exemption. A gate that has stopped firing looks exactly like a gate with nothing to say.
The parser now keeps command position across a reserved word, and only when the word is itself
in command position - so
echo do git commitis still an echo. Nothing else moves:--no-verifyisstill the bypass and now reaches inside those constructs too, the per-commit counting that stops a
later bypass exempting an earlier commit is untouched, and a payload with no commit still exits 0.
Same-defect sweep
The defect class is "a shell-aware parser whose notion of command position is operators only".
Checked all four sibling hooks in
.claude/hooks/. None can carry it - and not because theyhandle reserved words, but because none of them track command position at all:
check-squash-subject.shmatches with a regex over the raw command, andcheck-merge-outstanding-work.sh,remind-inflight-on-push.shandcheck-history-rewrite.shscanevery token from a flat
shlex.split. That is the right posture for them: each can only everrefuse a merge or a rewrite, so over-matching costs a spurious deny carrying an actionable
message.
pre-commit-gate.shis the only one that can exit 0 on a miss, which is why it is the onlyone that tracks command position - and therefore the only one that could have this blind spot.
Running that sweep found a false claim in
docs/agent-harness.md, fixed in the second commit:the file said
check-squash-subject.sh"only matchesghin command position, soecho gh pr merge ...is text rather than a merge". It does not, and the echo is denied. Thenear-miss case in
bin/test-check-agent-hooks.shthat reads like a command-position test passes foran unrelated reason - its quoting makes
shlexraise inside the matched slice. The behaviour isleft alone (safe direction for that hook) and recorded under Known gaps with the reason it stays;
only the promise the code does not keep is removed, so the next fix is not built on it.
What was verified about the
ifregistrationThe script-side guard exists because the registration cannot be trusted, so this was measured rather
than assumed. On Claude Code 2.1.231,
if: "Bash(git commit *)"on the hook object filters aplain
echo MARKER_OKcorrectly (hook did not run), but does not filter a compound command:reached an always-deny hook in a scratch project and was blocked. That is the live misfire this
harness has now been bitten by twice. The prefix matcher stays - it is still the cheap filter for
the common case, and
docs/agent-harness.mdexplains why this hook keeps it - but the script decidesfor itself.
Second wave: the newline and
functiongaps, found by review on this PRThe automated review traced two more members of the same defect class in the same function, and
both were real (verified by live repro before fixing):
shlex's default whitespace includes\n, so a line break produced no token at all andat_commandcarried over from the previousline -
git add -Aon line one followed bygit commit -m wipon line two counted zerocommits, and zero means skip. Fixed by moving
\nfromwhitespaceintopunctuation_charsand adding an
is_separator()predicate that also recognises the glommed punctuation runs shlexproduces (a bare
"\n",";\n\n","&&\n"). The commit-extent scan stops at newlines too,so a later line's
--no-verifycannot exempt an earlier commit.function deploy { git commit -m x; }was invisible - the function name consumed commandposition before the
{introducer could reopen it (thefoo() { ... }spelling was only evercaught because bare
()are operator tokens). Fixed by skipping the keyword and the name,leaving position open for the brace.
A follow-up review pass confirmed both fixes by independent token-level trace and found no further
gaps in the class.
Chaos-lane sightings recorded in passing
Chaos Pain Suite(not a required check) went red on two heads of this branch with the knownseed-dependent
CLASS2_STALLsignature tracked indocs/inflight/bug-857-family.md. This branchcontains no Java; the seeds and two same-day timing rule-outs of the runner-contention hypothesis
are recorded there as the sixteenth sighting and its addendum.
Testing
Seven cases added to
bin/test-check-agent-hooks.sh, four of them proven red against the unfixedhook. The three that stay green are the near-miss
docs/agent-harness.mdrule 3 asks for - each isone
gitaway from being gated: a compound non-commit command withfor,ifand$(...); areserved word in argument position; and a
--no-verifybypass inside a construct. The second wave added seven more (five proven red against the pre-fix hook): the newline-joined forms of the semicolon-joined constructs, the two-sequential-lines shape, the cross-line--no-verifyextent case, a multiline non-commit negative, and thefunctionbody. Full suite: 173 pass, 0 fail - locally and in theagents: hook self-testsCI lane.Checklist
docs/agent-harness.md: the hook table row and the wiring bullet said theifregistration is what scopes this hook, which is the belief this PR repairs.
bin/test-check-agent-hooks.sh, four red against the oldhook, three near-misses that must stay green.