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 ;;
esacfiMeasured
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:
| command | verdict | wanted |
|---|
grep -rn "cd x && git stash pop" .claude/ | allow | allow — control, pinned in the self-test today |
grep -rn "he said \"cd x && git stash pop\" once" .claude/ | BLOCK | allow |
echo "he said \"x && git stash pop\" once" | BLOCK | allow |
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
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'ssplit_segments()has no backslash branch on itsin-quote path:
Inside
"…"a\"is a literal quote and the quoted region stays open. Reading it as closingthe 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, whosesplit_segments()carries the branch today: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:
grep -rn "cd x && git stash pop" .claude/grep -rn "he said \"cd x && git stash pop\" once" .claude/echo "he said \"x && git stash pop\" once"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 guard that blocks
grepfor quoting a rule is the failure mode #4890 named, and the one thehook'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'ssplit_segments(), and add thetwo rows above to
.claude/hooks/guard-shared-stash.selftest.sh— that file's own instruction is tore-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.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