Skip to content

[finding] guard-main-checkout-bash.sh does not strip # comments, so a comment containing ; and > is analysed as a write and blocks the command #10720

Description

@claude

Hit directly while running a pure-curl command from the devx PM seat. .claude/** is a
governed surface, so this is filed as a finding rather than fixed. Filed unassigned.

What happened

The command was four curl calls plus one bash comment:

# sitting 1 landed; card stays open and ready for sitting 2 -> pm:dispatched must go, pm:queue returns

The hook blocked it:

⛔ Blocked: this Bash command WRITES into the shared PRIMARY checkout, not a worktree.
command: card stays open and ready for sitting 2 -> pm:dispatched must go, pm:queue returns
target: pm:dispatched
repo: /home/user/objectstack

There is no write in that command. The hook read the comment.

Mechanism, from the hook's own source

  1. split_segments() (:128-139) splits on ; | & ( ) { } \n, honouring quotes. My comment
    contains a ;, so it is cut in two and the trailing clause becomes its own "segment".
  2. Nothing strips # comments. The hook strips heredoc bodies (strip_heredocs, :97)
    and it makes separators inert inside quotes — but a # comment is neither, so the tail of the
    comment is analysed as a command.
  3. That tail contains ->. The tokenizer marks > as an unquoted redirection operator
    (:171, :244-245) and takes the next token, pm:dispatched, as the redirect target.
  4. target_is_shared_checkout() (:189) resolves that relative target against the cwd — the
    shared primary checkout — and the guard fires.

Why this is a defect rather than acceptable over-blocking

The hook's own header declares "PRECISION OVER RECALL" (:32) and deliberately declines
several genuinely-ambiguous shapes: node -e, awk >, targets containing $VAR / globs,
relative targets with no cwd. The intent is explicit at :125:

"A separator inside '…' or "…" does NOT split, so writing about the ban is never
caught."

That is exactly the property a comment should have, and it is implemented for quotes and for
heredocs but not for #. A comment is the purest case of writing about a write rather than
performing one.

The concrete harm is the remedy the block points at. The message's only escape is
OS_ALLOW_MAIN_EDITS=1, which disables the guard for the whole command. A false positive on
a harmless comment trains the operator to reach for the total-override flag on commands that
genuinely do not need it — and the next command it is reached for might. A guard whose false
positives are cheapest to resolve by turning it off is being pushed in the wrong direction.

Same family as #5790, which is why this hook exists at all: the Edit-only guard let the
identical write slip through as a shell command. This is that lesson's mirror image — the shell
parser now sees writes the Edit hook could not, including ones that are not there.

Direction (not a decision)

Strip # comments before split_segments(), with the same care strip_heredocs() already
takes: a # only begins a comment at the start of a word, and never inside '…', "…", or a
heredoc body — so curl 'https://…/#frag', sed 's/#//' and grep '#' must be untouched. The
existing quote-aware scanner already has the state needed to decide that.

⛔ Not proposing any change to what the hook blocks once it has a real command. The recall side
is right; this is only about what counts as a command.

Repro

Any Bash command containing a comment with both a ; and a > — e.g.
# step one; then a -> b — from a cwd inside the shared checkout.

Refs

.claude/hooks/guard-main-checkout-bash.sh:32, :97, :125, :128-139, :171, :189,
:244-245 · #5790 (why the Bash-side guard exists) · governed surface, Prime Directive #14


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