Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .claude/hooks/guard-shared-stash.selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ expect allow 'echo a\ b'
expect allow 'echo \\ ; grep -n worktree README.md'
expect allow 'echo \" ; git stash list'

echo "== an escaped \\\" INSIDE a double-quoted word does NOT close it =="
# Inside "…" a backslash is special only before " \ $ ` — so an escaped `\"` is a literal
# quote and the quoted region stays OPEN. A pass that reads it as closing goes "outside
# quotes" while bash is still inside: separators behind it split where bash would not, and
# the tail of a pure READ becomes a segment of its own, judged on its own head word. The
# single-level control above (`grep -rn "cd x && git stash pop" .claude/`) is the same
# command without the nested escape, so it isolates that escape as the only difference.
expect allow 'grep -rn "he said \"cd x && git stash pop\" once" .claude/'
expect allow 'echo "he said \"x && git stash pop\" once"'
# Precision twins — this is not a blanket "ignore whatever follows a backslash". Once the
# escapes pair up the region really is closed, and the stash behind it is still caught.
expect block 'echo "he said \"x\"" && git stash pop'
expect block 'echo "he said \"x\"" ; git stash drop'
expect block 'echo "quoted" && git stash pop'
# `\\` inside "…" is an escaped backslash, so the NEXT `"` still closes the region; without
# the `\` arm of the escapee list that closing quote is eaten and the stash rides through.
expect block 'echo "a \\" ; git stash pop'
# Inside '…' nothing is special — that asymmetry is what the q='"' gate pins. Applied to
# single quotes the branch would swallow the closing `'` and pass the stash behind it.
expect block "echo 'a \\' ; git stash pop"

echo "== escape hatch =="
expect allow 'git stash pop' OS_ALLOW_STASH=1

Expand Down
23 changes: 21 additions & 2 deletions .claude/hooks/guard-shared-stash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
# for anyone who means it. Widening it to string-match anywhere in the command would block
# every `grep "git stash"` run against this very file.
#
# Self-test (41 cases, no network, no build): .claude/hooks/guard-shared-stash.selftest.sh
# 41 = 39 `expect ` lines + 2 inline specials (empty-tool_input fail-open, no-jq fallback).
# Self-test (48 cases, no network, no build): .claude/hooks/guard-shared-stash.selftest.sh
# 48 = 46 `expect ` lines + 2 inline specials (empty-tool_input fail-open, no-jq fallback).
# Re-derive when the matrix changes: `grep -c '^expect ' <selftest>` + 2, and the run's own
# tail prints the total ("N passed, N failed") — keep this number equal to it.

Expand Down Expand Up @@ -83,12 +83,31 @@ fi
# a mere argument of `echo`. That is a fail-OPEN in the backstop for the one rule whose
# breach silently corrupts ANOTHER agent's work (objectstack#11131, the same defect the
# sibling hook guard-main-checkout-bash.sh carried; objectui#6042).
#
# INSIDE "…" the rule inverts: there a backslash is special only before " \ $ ` , and an
# escaped `\"` is a literal quote that leaves the region OPEN. A pass that reads it as
# CLOSING goes outside quotes while bash is still inside, so separators behind it split
# where bash would not: the tail of a pure READ becomes a segment of its own, judged on its
# own head word — a false BLOCK on a command that touches no stash, which is exactly what
# the paragraph at the top of this section promises can never happen. The same gap fails
# OPEN in the other direction: once the escapes pair up the quoted region is left hanging
# and a real `git stash` behind it rides through as a mere argument. Inside '…' nothing is
# special, hence the q='"' gate. This is the in-quote half of the backslash rule, in the
# same shape and with the same escapee list as guard-main-checkout-bash.sh's
# split_segments() carries; that guard's `word` bookkeeping has no analogue here because
# this pass has no comment rule to track word starts for.
segments=()
split_segments() {
local s="$1" seg="" q="" ch i n=${#1}
for ((i = 0; i < n; i++)); do
ch="${s:i:1}"
if [ -n "$q" ]; then
if [ "$q" = '"' ] && [ "$ch" = '\' ] && [ $((i + 1)) -lt "$n" ]; then
case "${s:i+1:1}" in
'"' | '\' | '$' | '`')
seg+="$ch" ; i=$((i + 1)) ; seg+="${s:i:1}" ; continue ;;
esac
fi
seg+="$ch"
[ "$ch" = "$q" ] && q=""
continue
Expand Down
Loading