Uh oh!
There was an error while loading. Please reload this page.
fix(hooks): give guard-shared-stash's split_segments() the backslash branch #11131 closed next door - #11803
fix(hooks): give guard-shared-stash's split_segments() the backslash branch #11131 closed next door#11803claude[bot] wants to merge 1 commit into
Conversation
…branch
Outside quotes a backslash escapes the next character, so an escaped `\"` opens no
quoted region at all. split_segments() had no backslash branch, so it read the `"`
as opening a region that never closed, went inert for every separator behind it,
and collapsed the whole command into one segment whose head word was the harmless
one — leaving a real `git stash pop` to be read as a mere argument of `echo`.
This is the fail-OPEN backstop gap in the enforcement behind the ⛔ Never `git stash`
Prime Directive, the rule whose violation swapped two parallel agents' in-flight work
(objectui#3430).
Measured on THIS repo's copy (the card's reading was taken on objectui's), fed as the
PreToolUse payload shape {cwd, tool_name:"Bash", tool_input:{command}}:
git stash (control) before: exit 2 blocked after: exit 2 blocked
git stash pop (control) before: exit 2 blocked after: exit 2 blocked
echo \" ; git stash before: exit 0 ALLOWED after: exit 2 blocked
echo \" ; git stash pop before: exit 0 ALLOWED after: exit 2 blocked
echo \" && git stash pop before: exit 0 ALLOWED after: exit 2 blocked
printf \" ; git stash drop before: exit 0 ALLOWED after: exit 2 blocked
Same repair guard-main-checkout-bash.sh's split_segments() took in #11131, in the same
shape. That guard's `word` bookkeeping has no analogue here: this pass has no comment
rule, so there are no word starts to track. The #11133 comment-heredoc half does not
apply either — there is no strip_heredocs() pass here to carry that defect.
This fixes a parse that was WRONG, not one that was uncertain. The hook's documented
fail-open posture for what it cannot parse (wrapped invocations: bash -c, xargs, ssh)
is unchanged and deliberate.
Self-test: 32 -> 44 passed, 0 failed. The six new block cases were run against the
pre-fix hook and all six FAIL there (38 passed, 6 failed), so they are discriminating
rather than decorative; the six precision twins pass in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZxos-steve
commented
Aug 24, 2026
ACCEPT (advisory — governed surface, so the merge decision is Reproduced, live, on |
Fixes#11738
.claude/hooks/guard-shared-stash.sh'ssplit_segments()had no backslash branch. Outsidequotes a backslash escapes the next character, so an escaped
\"opens no quoted region.Without the branch the pass read that
"as opening a region that never closed, went inertfor every separator behind it, and collapsed the whole command into a single segment whose
head word was the harmless one — leaving a real
git stash popto be read as a mere argumentof
echo.This is the enforcement behind the ⛔ Never
git stashPrime Directive — the rule whoseviolation swapped two parallel agents' in-flight work with a success message (objectui#3430).
The rule outranks the hook, so this was a backstop gap rather than a live breach; the backstop
exists precisely for the agent who does not know the rule.
The card's premise held — measured here, not inherited
The card's reading was taken on objectui's copy and asked whoever took it to probe this
repo's copy independently rather than trust the declared parity. Probed, on
de1cb8def, fedas the PreToolUse payload shape
{cwd, tool_name:"Bash", tool_input:{command}}:git stash(control)git stash pop(control)echo \" ; git stashecho \" ; git stash popecho \" && git stash popprintf \" ; git stash dropecho \"⏎git stash popecho \" ; echo hello(precision)echo a\ b(precision)echo \\ ; git status(precision)The controls are load-bearing: without them "ALLOWED" could mean the harness never reached the
guard. And the shape really does execute —
bash -c 'echo \" ; echo SECOND_COMMAND_REALLY_RAN'prints
"and thenSECOND_COMMAND_REALLY_RAN.The fix follows #11131, and says where it doesn't
The branch is the one
guard-main-checkout-bash.sh'ssplit_segments()took in #11131 (landedin #11278), in the same shape. Two deliberate deviations, both stated in the file:
word=1. No analogue here — this pass has no comment rule,so there are no word starts to track. Porting the assignment would have added a dead variable.
strip_heredocs()pass here tocarry that defect. Not ported, per the card.
This fixes a parse that was wrong, not one that was uncertain. The hook's documented
fail-open posture is unchanged and untouched — its header states it explicitly ("Anything this
cannot parse fails OPEN — a guard that blocks work it does not understand gets disabled, and
then it guards nothing"), along with the named boundary it keeps (wrapped invocations:
bash -c,xargs,ssh host '…'). So the stash guard's intended posture is the same as the checkoutguard's, declared in its own header rather than assumed from the sibling. Making the parse
correct is strictly better than either posture and does not trade against that boundary.
Non-vacuity: the new cases fail before the fix
Twelve cases added (6 block + 6 precision twins), 32 → 44. The new self-test was run against the
pre-fix hook — lifted from
de1cb8definto a scratch dir, so the worktree was never mutated:All six failures are the six new block cases; all six precision twins pass in both directions,
so they pin precision rather than padding the count. The ablation was confirmed on disk before it
was read, each zero against a positive control: the branch literal
'\')appears 1× in thefixed hook and 0× in the pre-fix copy, and the new comment sentence 1× / 0×.
Verification
Gates re-run on the final commit
bc9400bcd, all green, quoting each gate's own verdict line:guard-shared-stash.selftest.sh—44 passed, 0 failedcheck:nul-bytes—check-nul-bytes: OK (scanned 6577 text file(s) … no raw ASCII control bytes)check:agent-test-spelling— self-test + sweep, exit 0check:doc-authoring—✓ doc authoring guard: 389 files cleancheck:pm-governed-merges—✓ check-governed-merges --self-test: 129 assertionscheck:skill-frame-sync—✓ … 4 copies of the decision frame are structurally isomorphiccheck:doc-formula-expressionswas narrowed, and the narrowing measured (it needs a workspaceinstall this bash-only change does not otherwise need): its population comes from the gate's own
source —
ROOTS = ['.claude','docs','skills','content']with the walker keeping only.md/.mdx(plus
.ts/.tsxunderpackages/spec/src). That population is 1671 files at this commit;it contains 0
.shfiles and neither changed file, while the positive control shows the walkreally does reach
.claude/(25.mdfiles there). My diff adds and removes 0.md/.mdxpaths, so the population is identical before and after. CI runs the full farm regardless.
No changeset:
.claude/**is internal agent tooling and publishes nothing —skip-changeset.Scope notes
split_segments():this one and
guard-main-checkout-bash.sh(already repaired by [finding] guard-main-checkout-bash.sh: an unquoted backslash still makes tokenize() and split_segments() disagree, and a realsed -iinto the shared checkout is allowed through #11131).guard-main-checkout.shreads.tool_input.file_path, not commands — it parses no shell atall and structurally cannot carry the defect.
rather than folded in — it is the
#10406half (an escaped quote inside a double-quotedword), not the
#11131half this card scopes to. It is a false BLOCK, not a fail-open:grep -rn "he said \"cd x && git stash pop\" once" .claude/is blocked today, though thefile's own header promises writing about the ban is never caught by it.
repos, separate PRs, not folded here.
.claude/**): this PR stays draft for human merge. Review requestedfrom @os-zhuang.
Generated by Claude Code