Skip to content

[finding] guard-main-checkout-bash.sh: a heredoc introducer NAMED inside a comment makes strip_heredocs() swallow the real commands after it #11133

Description

@os-zhuang

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:

  1. Teach strip_heredocs() enough word/quote state to ignore a <<WORD that sits after an unquoted word-start # on the same line — narrow, and it keeps the existing pass order.
  2. 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.shstrip_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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions