Repository navigation
Description
Measured while implementing #10570 (PR #11129). Out of that card's adjudicated scope: #10570 teaches the comment rule to the two quote-aware passes (tokenize(), split_segments()), while this defect lives in the third pass, strip_heredocs(), which runs before them. Filed unassigned and NOT fixed in that PR. Dedup performed first: no existing card covers it.
What was measured
Confirmed against origin/main at 95437e7d2 (so it pre-dates PR #11129) and re-confirmed as unchanged by that PR, from a cwd inside the shared primary checkout:
# use cat > /tmp/n <<EOF for notes
sed -i s/a/b/ pkg/x.ts
-> hook-exit=0 (ALLOWED)
# plain prose comment
sed -i s/a/b/ pkg/x.ts
-> hook-exit=2 (blocked)
The two commands differ only in the wording of the comment. The first mentions a heredoc; the real in-place write on the next line is then never analysed.
Mechanism
strip_heredocs() scans line by line for <<WORD and drops every following line until the delimiter reappears. It runs on the raw command text and models neither quotes-per-line nor comments, so a <<EOF occurring inside prose registers as a real introducer. EOF never appears as a line of its own, so the pending delimiter is never satisfied and every remaining line — including real commands — is dropped before either quote-aware pass sees it.
This is the fail-OPEN direction, the mirror of #10570's false block, and it comes from the same root cause: a comment is text, and a pass that does not model comments will read one as structure.
Why it is not folded into #10570
#10570's grading names the two quote-aware passes and rules that what the hook blocks once it has a real command must not change. Teaching strip_heredocs() the comment rule is a change to a third pass with its own ordering constraint, and the ordering is the interesting part: comments cannot simply be stripped BEFORE heredocs, because an unbalanced apostrophe inside a heredoc BODY would then desynchronise a quote-aware comment scanner across the following lines. That is a design question rather than a mechanical edit, which is what makes it a separate card rather than a rider.
Direction (not a decision)
Two candidates worth weighing rather than one to adopt:
- Teach
strip_heredocs()enough word/quote state to ignore a<<WORDthat sits after an unquoted word-start#on the same line — narrow, and it keeps the existing pass order. - Make heredoc stripping and comment stripping one scanner with shared state, so neither pass can disagree with the other about what is text. Larger, and it is the shape the hook's header already argues for when it lists its layers.
Either way the fix belongs with self-test cases in both directions, as the matrix already does for quotes, heredocs and (after PR #11129) comments.
Refs
.claude/hooks/guard-main-checkout-bash.sh — strip_heredocs() · #10570 / PR #11129 (found here) · #10720 (closed duplicate of #10570, carries the mechanism walkthrough) · #5790 (why the Bash-side guard exists) · governed surface
Generated by Claude Code
Activity
os-zhuang commentedon Aug 23, 2026
Claim: family member — full claim, serial-constraint check and fold answer live on the anchor #11131 (session
session_01RMTpSRF5CjMmQBFfPtPCwJ, branchclaude/issue-11131-guard-bash-pass-agreement). This member's independent criterion: the comment-named heredoc introducer no longer swallows the real commands after it; both directions pinned in the selftest; own commit;Fixes #11133line in the PR.Generated by Claude Code
fix(hooks): a heredoc introducer named inside a comment introduces no…
claude commentedon Aug 23, 2026
Implemented as a family member in draft PR #11278 (branch
claude/issue-11131-guard-bash-pass-agreement), which carries aFixes #11133line. This member’s criterion: the comment-named heredoc probe flips ALLOWED->blocked (hook-exit 0 -> 2) with real heredoc behaviour untouched; both directions pinned in the selftest; own commit 389fddc. Full os-dev report lives on the anchor #11131. Governed surface: draft, human merge only.Generated by Claude Code
Generated by Claude Code
fix(hooks): make guard-main-checkout-bash agree with itself on backsl…
fix(hooks): give guard-shared-stash's split_segments() the backslash …
fix(hooks): give guard-shared-stash's split_segments() the backslash …