Repository navigation
fix(gates): subagent guard reads an escaped letter the way the shell does (GH #317) - #335
Merged
Merged
Conversation
…ell does (GH #317) Outside quotes a shell drops a backslash before an ordinary character, so `g\it stash`, `git re\set`, `git -\C . stash`, `e\nv git stash` and `ba\sh -c 'git stash'` all run git, while the quote mask kept the backslash and the anchor scans missed them. A new `_mask` (the quote mask, then one pass per word) drops that backslash for the anchor scans of the command, shell bodies and substitution bodies. The freed blanks go to the word's front, so each word still ends at its raw offset. A pair stays (`\\` is a literal backslash), and so does a backslash before a character it makes literal (`\{`, `A\=1`, `\*`). The heredoc scans keep the plain mask, whose offsets they need. Closes #317
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #317.
What
Outside quotes a shell drops a backslash before an ordinary character, so
g\it stash,git re\set,git -\C . stash,e\nv git stash,ev\al 'git stash'andba\sh -c 'git stash'all run git in sh, bash 3.2, bash 5, dash, zsh and ksh. The quote mask keeps the backslash, so the subagent guard's anchor scans missed them.subagent-git-guard.pygets_mask: the quote mask, then one pass per word that drops a backslash before an ordinary character. The freed blanks go to the word's front, so each word still ends at its raw offset (the shell-body lookup reads the raw command there). A backslash pair stays (\\is a literal backslash:g\\it stashruns nothing), and so does a backslash before a character it makes literal (\{,A\=1,\*,\$, quotes)._maskis used for the command, shell bodies and substitution bodies.Deviation from the issue's wording: the issue points at the quote mask. The drop lives in the guard instead of
_quotemask.py, because the heredoc scans need the plain mask's offsets: a padded<<\EOFshifts the<<and the heredoc parser refuses the command (theheredoc-scan-sees-padded-wordsmutant below shows that deny). It also leavestest-integrity.pyunchanged.irrecoverable.py: its git, rm and push rules tokenize withshlex(posix), which already drops these backslashes (g\it checkout .,git che\ckout .,git reset --ha\rd,git pu\sh -fare denied). Its nested-spawn regex anchor has the gap (cl\aude -p x,claude -\p x,e\nv claude -p x) and the #318 one ({claude -p x;}): filed as #322, not widened here.Tests
tests/hooks/test-subagent-git-guard.shsection (24): 28 deny rows (each runs git stash/reset/clean in all six shells,{g\it stash;}in zsh), 11 allow controls (each runs nothing in any shell). All deny rows were red before the fix; one control (git stash l\ist) was a develop false deny and is now allowed.Mutation check: 16 gate mutants, 16 killed (no drop, drop whole runs, empty keep set, keep set without
=or{, no pad, pad at the end, word token taking<>or blanks, bodies / shell bodies / substitution bodies not unescaped, value globals ignored, stash list carve-out gone, blank as a command start, heredoc scan on the padded mask). Every new row flips under at least one mutant.Differential fuzz (real shells as oracle, any deny wins)
Own generator (escaped letters in wrapper, git, global flags and subcommand words, 0 to 3 backslashes per letter, inside bodies, substitutions and compounds), 3993 cases against current develop (after #321):
git stash l\ist,stash sh\ow) that develop denied by mistake. None runs a destructive git in any shell./usr/bin/env git ..., gate: subagent guard does not see a wrapper written as a path (/usr/bin/env git stash) #320), 75 are a second parse (eval g\\\it stash,bash -c "g\\it stash",eval nice\ git reset), where a backslash pair becomes an escape on the second parse: filed as gate: subagent guard misses an escape that only appears after eval or a double-quoted shell body parses the text again #327.=makes a command word). Safe direction.gitand a backslash: 0 newly denied, 0 newly allowed, worst case 0.24 s.Validation
test-subagent-git-guard.sh597/597,test-gates.sh1014/1014,test-quotemask.sh39/39,test-test-integrity.sh30/30, harness-audit 0 CRIT,run-gauntlet.shall layers passed.