Found while working #9830 (PR #10569). Out of that card's scope — it edits one workflow file and touches nothing under .claude/. Filed unassigned.
What was observed
guard-main-checkout-bash.sh does not recognise shell comments, so a > inside a # comment is read as a redirection and the whole command is BLOCKED. A comment cannot write anything, so this is a false block — the fail-closed direction, opposite to the fail-open concern recorded on #10409.
Reproduced against the committed hook, with the control that isolates the comment as the cause:
$ python3 -c "import json; print(json.dumps({'cwd':'/home/user/objectstack','tool_input':{'command':'# rename foo -> bar\necho hello'}}))" \
| bash .claude/hooks/guard-main-checkout-bash.sh
⛔ Blocked: this Bash command WRITES into the shared PRIMARY checkout, not a worktree.
command: # rename foo -> bar
target: bar
repo: /home/user/objectstack
hook-exit=2
$ python3 -c "import json; print(json.dumps({'cwd':'/home/user/objectstack','tool_input':{'command':'echo hello'}}))" \
| bash .claude/hooks/guard-main-checkout-bash.sh
hook-exit=0
Same command, same cwd; the only difference is the comment line. grep -n "comment\|'#'" .claude/hooks/guard-main-checkout-bash.sh and the same grep over guard-main-checkout-bash.selftest.sh both return nothing — there is no # handling in either the tokenizer or the segmenter, and no self-test probe for the shape.
How it actually bit
A single Bash call carrying three labelled sub-runs, each preceded by an explanatory comment. One comment read # (c) a path already in the INDEX that the allowlist does not name -> the BAD re-check. The hook reported target: the and repo: /home/user/objectstack, blocking a command whose only file writes were into $TMPDIR and a scratchpad. Cost: one retry with the arrow removed from the prose. It looks exactly like the guard catching a real violation, which is what makes it worth a card rather than a shrug — the message names the primary checkout and a plausible-looking target.
Why it matters, and the honest severity
Low, and self-announcing. The failure is loud, and the workaround (reword the comment) is immediate. It never lets a write through.
The reason to fix it anyway is the hook's own stated philosophy, quoted from its header:
PRECISION OVER RECALL. Recognising a write target inside an arbitrary shell command is heuristic in a way that Edit's file_path is not, and a guard that blocks work it does not understand gets switched off — after which it guards nothing. So this hook only claims shapes it can read with confidence, and everything else fails OPEN.
A comment is not a shape the hook reads with low confidence — it is a shape it does not model at all, and it currently claims one with false confidence. Prose containing ->, =>, 2> or N > M is ordinary in explanatory comments, so the shape recurs.
Shape of a fix (not a ruling)
Give both quote-aware passes (tokenize() and split_segments()) the shell's comment rule: outside quotes, an unquoted # that starts a word begins a comment that runs to the next newline. Note the word-start condition — foo#bar and ${x#y} are not comments — so this is not a bare "strip from the first #".
Then extend guard-main-checkout-bash.selftest.sh in both directions, the discipline #10409 already argues for on this hook: a comment containing > must NOT block, and a real redirect on a line after a comment must still block.
Relationship to #10409
Same script, same two passes, different defect and opposite failure direction. #10409 is about a backslash outside quotes making the two passes disagree, and its direction is a missed write (fail-open). This one is a false block (fail-closed) from a construct neither pass models. Fixing #10409's backslash case does not address comments, so this is a sibling rather than a sub-issue — but the two land in the same function pair and would be cheaper to fix and self-test together.
Refs: #10409 (the sibling observation on this script) · #10406 / #10247 (the last change to these passes) · #9830 / PR #10569 (found here).
Generated by Claude Code
Found while working #9830 (PR #10569). Out of that card's scope — it edits one workflow file and touches nothing under
.claude/. Filed unassigned.What was observed
guard-main-checkout-bash.shdoes not recognise shell comments, so a>inside a#comment is read as a redirection and the whole command is BLOCKED. A comment cannot write anything, so this is a false block — the fail-closed direction, opposite to the fail-open concern recorded on #10409.Reproduced against the committed hook, with the control that isolates the comment as the cause:
Same command, same cwd; the only difference is the comment line.
grep -n "comment\|'#'" .claude/hooks/guard-main-checkout-bash.shand the same grep overguard-main-checkout-bash.selftest.shboth return nothing — there is no#handling in either the tokenizer or the segmenter, and no self-test probe for the shape.How it actually bit
A single Bash call carrying three labelled sub-runs, each preceded by an explanatory comment. One comment read
# (c) a path already in the INDEX that the allowlist does not name -> the BAD re-check. The hook reportedtarget: theandrepo: /home/user/objectstack, blocking a command whose only file writes were into$TMPDIRand a scratchpad. Cost: one retry with the arrow removed from the prose. It looks exactly like the guard catching a real violation, which is what makes it worth a card rather than a shrug — the message names the primary checkout and a plausible-looking target.Why it matters, and the honest severity
Low, and self-announcing. The failure is loud, and the workaround (reword the comment) is immediate. It never lets a write through.
The reason to fix it anyway is the hook's own stated philosophy, quoted from its header:
A comment is not a shape the hook reads with low confidence — it is a shape it does not model at all, and it currently claims one with false confidence. Prose containing
->,=>,2>orN > Mis ordinary in explanatory comments, so the shape recurs.Shape of a fix (not a ruling)
Give both quote-aware passes (
tokenize()andsplit_segments()) the shell's comment rule: outside quotes, an unquoted#that starts a word begins a comment that runs to the next newline. Note the word-start condition —foo#barand${x#y}are not comments — so this is not a bare "strip from the first#".Then extend
guard-main-checkout-bash.selftest.shin both directions, the discipline #10409 already argues for on this hook: a comment containing>must NOT block, and a real redirect on a line after a comment must still block.Relationship to #10409
Same script, same two passes, different defect and opposite failure direction. #10409 is about a backslash outside quotes making the two passes disagree, and its direction is a missed write (fail-open). This one is a false block (fail-closed) from a construct neither pass models. Fixing #10409's backslash case does not address comments, so this is a sibling rather than a sub-issue — but the two land in the same function pair and would be cheaper to fix and self-test together.
Refs: #10409 (the sibling observation on this script) · #10406 / #10247 (the last change to these passes) · #9830 / PR #10569 (found here).
Generated by Claude Code