Skip to content

guard-shared-stash.sh's split_segments() misreads an escaped quote INSIDE a double-quoted word — the #10406 half, a false BLOCK on read-only commands #11804

Description

@claude

Found while implementing #11738 (PR #11803) and deliberately not folded into it: that card
scopes to the #11131 half of the backslash rule (an escape outside quotes), and this is
the #10406 half (an escape inside a double-quoted word). Same function, opposite failure
direction, different upstream fix — so it gets its own card rather than widening a governed-surface PR.

The observation

.claude/hooks/guard-shared-stash.sh's split_segments() has no backslash branch on its
in-quote path:

if [ -n"$q" ];then
seg+="$ch"
[ "$ch"="$q" ] && q=""# <- a \" here CLOSES the quote; in bash it does notcontinuefi

Inside "…" a \" is a literal quote and the quoted region stays open. Reading it as closing
the region flips the pass to "outside quotes" while the real shell is still inside, so separators
behind it split where bash would not — and the tail of a pure-read command gets promoted to a
segment of its own.

This is a false BLOCK, not a fail-open. It is the mirror of #11738 and the exact shape #10406
fixed in guard-main-checkout-bash.sh, whose split_segments() carries the branch today:

if [ "$q"='"' ] && [ "$ch"='\' ] && [ $((i +1))-lt"$n" ];thencase"${s:i+1:1}"in'"' | '\' | '$' | '`') seg+="$ch"; i=$((i +1)); seg+="${s:i:1}";continue ;;
esacfi

Measured

On de1cb8def, fed as the PreToolUse payload shape {cwd, tool_name:"Bash", tool_input:{command}}.
Both commands are pure reads that write nothing and touch no stash:

commandverdictwanted
grep -rn "cd x && git stash pop" .claude/allowallow — control, pinned in the self-test today
grep -rn "he said \"cd x && git stash pop\" once" .claude/BLOCKallow
echo "he said \"x && git stash pop\" once"BLOCKallow

The control is load-bearing: the already-pinned single-level case passes, so the guard is reached
and the difference is the nested escape alone.

Why it matters

The hook's own header promises the opposite of what it does here:

A separator inside '…' or "…" does NOT split, so writing about the ban is never caught by
the ban … (objectstack#4890's lesson — the PR writing a rule must not trip it.)

A guard that blocks grep for quoting a rule is the failure mode #4890 named, and the one the
hook's header says gets guards disabled — after which it guards nothing. Severity is a triage call,
not mine: a false block is loud and recoverable (OS_ALLOW_STASH=1), unlike #11738's silent hole.

Suggested fix

Port the #10406 in-quote branch from guard-main-checkout-bash.sh's split_segments(), and add the
two rows above to .claude/hooks/guard-shared-stash.selftest.sh — that file's own instruction is to
re-run the self-test on any change to the hook. After #11803 lands, this is the remaining half of
the backslash rule, so the two guards' split_segments() stay divergent until it is done.

⚠️.claude/** is a governed surface — draft PR, human merge.
⚠️ Cross-repo: objectui's copy is declared case-for-case identical, so it likely carries this too —
probe it rather than trusting the parity declaration (that is exactly what #11738 asked for, and
the parity did hold there).

Filed unassigned by the dev seat on #11738; domain:*, grading and queueing are the triage seat's.

Refs: #11738 · PR #11803 · #11131 · #10406 · PR #11278 · objectui#6042.


Generated by Claude Code


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