Repository navigation
fix(gates): a wrapper written as a path is the same wrapper (GH #320) - #342
Merged
Merged
Conversation
/usr/bin/env git stash and /usr/bin/env claude -p x ran the target while both gates matched only the bare wrapper word. Every wrapper word in the guard (walk, lead lookahead, lead step, chain command/exec) and rtk, plus irrecoverable.py's spawn anchor, now take an optional directory prefix. The prefix never starts with - and holds no =, < or >, so a token has one reading; the git word's \S*/ made the chain loops exponential (eval command + -/command x40 ran past 15 s).
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 #320.
What
A wrapper written as a path (
/usr/bin/env git stash,/usr/bin/nice claude -p x) runs the same program as the bare word, and both gates allowed it.subagent-git-guard.py: every wrapper word takes an optional directory prefix (_PATH), in the walk, in the walk's negative lookahead, in the lead-chain lookahead (_LEAD_WRAPPER_ALT) in itscommand/execstep (_LEAD_STEP), and in the chain'scommand/exec(_SHELL_PASS, found by the fuzz:eval /usr/bin/command git stash).rtktakes it too (_RTK_PREFIX, so/opt/homebrew/bin/rtk proxy git stashdenies;_CHAIN_RUN_REcharges it the same way).evalandbuiltinare shell builtins with no path form and are left alone.irrecoverable.py: its rule loop already compares basenames (/usr/bin/env git checkout .was denied). Only the nested-spawn anchor's_WRAPPER_ALTmatched the bare word; it takes the same prefix.Deviation from the issue
The issue suggested
(?:\S*/)?, the git word's prefix. I measured that and it made the guard exponential. A token could then be read two ways (a-flagor a path command, a redirection or a path wrapper), so the chain loops tried every split of a run:eval command+-/commandx 40 +ls,rtk+-x/rtkx 40 +lsand2>/x/envx 3000 +lseach ran past 15 s, and a timed-out hook allows. The prefix used here is one plain word that never starts with-and holds no=,<or>((?:(?!-)[^\s;&|()<>=]*/)?). Each token then has only one reading. All three shapes take 0.03-0.08 s, and each is a timing row in the test.Tests
tests/hooks/test-subagent-git-guard.sh: 655 pass (58 new). The drift loop now also checks/usr/bin/<word> git stashfor everyPREFIX_WRAPPERSword. Section 25 has 25 deny rows and 8 allow controls, each checked against sh, bash 3.2, bash 5, dash, zsh and ksh with a logging git stub. It also has 11 timing rows, including the three ambiguity shapes above.tests/hooks/test-gates.sh: 1024 pass (10 new): 7 spawn denies behind a path wrapper and 3 controls.command/exec, spawn anchor) or widens it (\S*/, a leading-allowed,</>allowed). The widening mutants are killed by the timing rows.harness-audit: 0 CRIT.run-gauntlet.sh: all layers pass.Differential fuzz (real shells as the oracle)
My own generator (4000 cases): bare, path and #317-escaped spellings of every wrapper, with flags, assignments, chain words (
eval,builtin,rtk proxy,eval command), decoy tokens, separators, brace/paren groups,$(...),bash -cand loops. The targets are git (stash/reset/clean plus read-only controls) and claude. The oracle runs each case in /bin/sh, bash 3.2, bash 5.3, dash, zsh and ksh with logging stubs, and any shell running a target makes the case deny-worthy.claude(/usr/bin/ti\me claude --bg x), or a zsh glued{beforeclaude.eval command rtk proxy nohup git stash,exec -a xin the chain, andbuiltin command claude ...in the spawn anchor.x/env,/usr/bin/envy) before a real one.builtinbefore a path (builtin rejects a path).rtk proxy -vor a nestedrtk, where rtk itself fails.execrows.-nas taking a value (an oracle-stub artifact), plus a gluedx=$(cat /usr/bin/time -p claude ...). That last one is develop's existing over-read of an assignment value, which now applies to a path too.Transcript replay
111,443 distinct Bash commands from local transcripts, run as subagent payloads through both gates on develop and on the branch:
irrecoverable.pyon both develop and the branch. That is a pre-existingKeyError: 'switch'in the ambiguity narrow pass, which the.shfails closed; filed as gate: irrecoverable.py crashes with KeyError 'switch' in the ambiguity narrow pass #340.