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
24 changes: 24 additions & 0 deletions .claude/hooks/guard-shared-stash.selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,30 @@ expect allow 'echo \" ; git stash apply abc1234'
expect allow 'echo a\ b'
expect allow 'echo \\ ; git status'

echo "== an escaped \\\" INSIDE a double-quoted word does NOT close it (#10406 half) =="
# The other direction of the same missing rule. Inside "…" a `\"` is a literal quote and the
# quoted region stays OPEN; reading it as closing flipped the pass to "outside quotes" while
# bash was still inside, so separators behind it split where bash would not and the tail of a
# pure READ became a segment of its own — a false BLOCK on a command that touches no stash.
# Both allow-cases below were BLOCKED before the in-quote branch existed, while the
# single-level control next door (`grep -rn "cd x && git stash pop" .claude/`) passed
# throughout: the guard was reached and the nested escape alone is the 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 — the branch must not become a blanket "ignore whatever follows a backslash".
# Once the quoted region really is closed, the stash behind it is still caught. The first two
# were ALLOWED before the branch existed (the same missing rule fails OPEN once the escapes
# pair up and the region is left hanging); the rest were blocked before and stay blocked.
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 case list that closing quote is eaten as an escapee and the stash rides through.
expect block 'echo "a \\" ; git stash pop'
# Inside '…' nothing is special, which is why the branch is gated on q='"'. Applied to single
# quotes it would swallow the closing `'` and let the stash behind it through.
expect block "echo 'a \\' ; git stash pop"

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

Expand Down
29 changes: 24 additions & 5 deletions .claude/hooks/guard-shared-stash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
# 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 (44 cases, no network, no build): .claude/hooks/guard-shared-stash.selftest.sh
# Self-test (51 cases, no network, no build): .claude/hooks/guard-shared-stash.selftest.sh

set -uo pipefail

Expand Down Expand Up @@ -97,16 +97,35 @@ fi
# as a mere argument of `echo` (#11738). Measured on this repo's copy before the fix:
# `echo \" ; git stash pop` was ALLOWED while a bare `git stash pop` was blocked.
#
# This is the 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 because this pass
# has no comment rule to track word starts for. Both characters are kept verbatim — this
# pass only SPLITS, and check_segment() re-reads the words afterwards.
# INSIDE "…" the rule inverts: there a backslash is special only before " \ $ ` , and an
# escaped `\"` is a literal quote that leaves the region OPEN. Without that branch this pass
# read it as CLOSING, so it went "outside quotes" while bash was still inside and separators
# behind it split where bash would not — the tail of a pure READ became a segment of its own
# and was judged on its own head word (#11804, the #10406 half). Measured on this repo's copy
# before the fix: `grep -rn "he said \"cd x && git stash pop\" once" .claude/` was BLOCKED
# while the single-level `grep -rn "cd x && git stash pop" .claude/` next to it was allowed.
# That is a false BLOCK on a command that touches no stash — precisely the failure the
# paragraph at the top of this section says must never happen. The same missing rule also
# fails OPEN once the escapes pair up: `echo "he said \"x\"" && git stash pop` left the
# region hanging and rode through. Inside '…' nothing is special, hence the q='"' gate.
#
# These are the same two repairs guard-main-checkout-bash.sh's split_segments() took in
# #11131 (outside) and #10406 (inside), in the same shape and with the same escapee list;
# that guard's `word` bookkeeping has no analogue here because this pass has no comment rule
# to track word starts for. Both characters are kept verbatim — this pass only SPLITS, and
# check_segment() re-reads the words afterwards.
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