Measured while implementing #10570 (PR #11129) in the same function pair. Out of that card's adjudicated scope — #10570 is the comment rule and its direction is a false BLOCK; this is the opposite direction, a missed write. Filed unassigned, and NOT fixed in that PR.
An older observation card on this same shape existed and is gone (404 on both API and web, invisible to search), so this is filed fresh rather than folded. Dedup performed first: a search for the guard plus backslash returns only #10570, the closed duplicate #10720, and #5790.
What was measured
Against the committed hook (origin/main at 95437e7d2, and unchanged by PR #11129), from a cwd inside the shared primary checkout:
command: echo \" ; sed -i s/a/b/ pkg/x.ts -> hook-exit=0 (ALLOWED)
command: sed -i s/a/b/ pkg/x.ts -> hook-exit=2 (blocked)
Same write, same cwd. The only difference is the escaped double quote ahead of it, and it is enough to let a real in-place edit of the shared checkout through in silence.
Mechanism
The two quote-aware passes handle an unquoted backslash differently:
tokenize() has a backslash branch outside quotes: it consumes the next character as a literal, so an escaped quote never opens a quoted region.
split_segments() has no such branch. Outside quotes a backslash is an ordinary character, so the " that follows it is read as OPENING a double-quoted region. Quote state then never closes, every separator after it becomes inert, and the whole command collapses into one segment whose command name is echo — after which the sed -i is just another argument and collect_targets() has nothing to report.
Layer 3 of the hook's header documents the mirror-image case (an escaped quote INSIDE a double-quoted word, #10247, which was a false block). This is the case outside quotes, and it fails the other way.
Why it is worth a card
The header's precision-over-recall philosophy explicitly accepts fail-open holes, and each accepted hole is enumerated there: wrapped invocations, interpreters whose writes live in their own source text, targets carrying expansions or globs, relative targets with no cwd. This shape is not one of them — the write target is a plain shell token in plain sight, and the guard would name it correctly if the two passes agreed. The hole is a disagreement between passes rather than a decision, which is exactly the class the hook's own self-test discipline exists to pin.
Severity is bounded by the same thing that bounds every hole here: the guard targets the reflexive sed -i an agent reaches for mid-task, not a determined evader, and nobody writes an escaped quote on purpose to slip past it. The realistic path in is incidental — a command that legitimately carries an escaped quote and also writes.
Direction (not a decision)
Give split_segments() the backslash branch tokenize() already has, so the two passes agree on what a quote is; then pin both directions in guard-main-checkout-bash.selftest.sh, whose matrix already carries the #10247 twins next door.
Refs
.claude/hooks/guard-main-checkout-bash.sh — split_segments() and tokenize() · #10570 / PR #11129 (found here) · #10247 (the inside-quotes mirror image) · #5790 (why the Bash-side guard exists) · governed surface
Generated by Claude Code
Measured while implementing #10570 (PR #11129) in the same function pair. Out of that card's adjudicated scope — #10570 is the comment rule and its direction is a false BLOCK; this is the opposite direction, a missed write. Filed unassigned, and NOT fixed in that PR.
An older observation card on this same shape existed and is gone (404 on both API and web, invisible to search), so this is filed fresh rather than folded. Dedup performed first: a search for the guard plus backslash returns only #10570, the closed duplicate #10720, and #5790.
What was measured
Against the committed hook (
origin/mainat95437e7d2, and unchanged by PR #11129), from a cwd inside the shared primary checkout:Same write, same cwd. The only difference is the escaped double quote ahead of it, and it is enough to let a real in-place edit of the shared checkout through in silence.
Mechanism
The two quote-aware passes handle an unquoted backslash differently:
tokenize()has a backslash branch outside quotes: it consumes the next character as a literal, so an escaped quote never opens a quoted region.split_segments()has no such branch. Outside quotes a backslash is an ordinary character, so the"that follows it is read as OPENING a double-quoted region. Quote state then never closes, every separator after it becomes inert, and the whole command collapses into one segment whose command name isecho— after which thesed -iis just another argument andcollect_targets()has nothing to report.Layer 3 of the hook's header documents the mirror-image case (an escaped quote INSIDE a double-quoted word, #10247, which was a false block). This is the case outside quotes, and it fails the other way.
Why it is worth a card
The header's precision-over-recall philosophy explicitly accepts fail-open holes, and each accepted hole is enumerated there: wrapped invocations, interpreters whose writes live in their own source text, targets carrying expansions or globs, relative targets with no cwd. This shape is not one of them — the write target is a plain shell token in plain sight, and the guard would name it correctly if the two passes agreed. The hole is a disagreement between passes rather than a decision, which is exactly the class the hook's own self-test discipline exists to pin.
Severity is bounded by the same thing that bounds every hole here: the guard targets the reflexive
sed -ian agent reaches for mid-task, not a determined evader, and nobody writes an escaped quote on purpose to slip past it. The realistic path in is incidental — a command that legitimately carries an escaped quote and also writes.Direction (not a decision)
Give
split_segments()the backslash branchtokenize()already has, so the two passes agree on what a quote is; then pin both directions inguard-main-checkout-bash.selftest.sh, whose matrix already carries the #10247 twins next door.Refs
.claude/hooks/guard-main-checkout-bash.sh—split_segments()andtokenize()· #10570 / PR #11129 (found here) · #10247 (the inside-quotes mirror image) · #5790 (why the Bash-side guard exists) · governed surfaceGenerated by Claude Code