Repository navigation
Conversation
📊 Automated PR Analysis
SummaryFixes RTK's permission gate so deny and ask rules match a command even when it's preceded by an environment variable assignment or wrapped in commands like timeout, nice, nohup, stdbuf, or a bare xargs, aligning it with Claude Code's own matching behavior. It adds a peel_for_matching function that reuses the rewrite's existing prefix-peeling logic plus a new match-only wrapper table for stdbuf/xargs, and applies it to both the deny and ask loops while intentionally leaving allow rules and git's… Review Checklist
Linked issues: #4281 Analyzed automatically by wshm · This is an automated analysis, not a human review. |
KuSh
marked this pull request as ready for review
September 26, 2026 18:03
KuSh
force-pushed
the
fix/4281-gate-peel
branch
2 times, most recently
from
September 26, 2026 21:22
c8b1821 to
c9a3f6e
Compare
KuSh
force-pushed
the
fix/4281-gate-peel
branch
from
October 5, 2026 21:22
c9a3f6e to
efff29f
Compare
…ers (#4281) Claude Code matches a deny or ask rule past any leading assignment and past a fixed set of wrappers — `timeout`, `time`, `nice`, `nohup`, `stdbuf`, `command`, `builtin`, `noglob` and a bare `xargs` — so `Bash(git push *)` stops `HUSKY=0 git push` and `timeout 30 git push` as well as `git push`. RTK's gate matched the segment as written and nothing else, so every one of those forms fell through to Default; under a host that treats Default as its own decision, as #3909's OpenClaw does, they auto-apply. The rewrite already looked through the same prefixes: `timeout 30 git push` became `timeout 30 rtk git push` while the gate did not recognise `git push` inside it. That was two readings of which command a line runs, and the gap between them is this bug. The deny and ask loops now also try `peel_for_matching`, which applies the rewrite's own peels — its env peel, its keyword prefixes, its wrapper table — to a fixpoint, so the gate and the rewrite cannot disagree again. It reads the segment after the split's leftover grammar is gone, since `{ HUSKY=0 git push; }` reaches the gate as `{ HUSKY=0 git push`, and the grammar-only reading stays a try of its own for a rule that names the assignment. Two wrappers the host strips were not in the rewrite's table, for a reason that still holds: `stdbuf` exists to unbuffer the wrapped command's output, which routing through rtk would buffer, and `xargs` runs whatever it is handed. Recognising a command is not the same decision as rewriting it, so they sit in a match-only table the rewrite never consults. `xargs` is peeled only in its bare form, as the host does; any option makes the wrapper grammar give up. The line is drawn at the shell. `env`, `exec` and `nocorrect` are peeled too, though the host's list stops short of them; each runs the next word as the command. A program's own arguments are not: `git -C . push` stays `git`, as the host documents, since reading past it would mean knowing every program's option grammar. Allow is deliberately left as it was. The host matches allow past known-safe variables only, and peeling a wrapper for it would let `timeout 30 <cmd>` inherit `<cmd>`'s allow rule — so the peel can only make a verdict stricter. Because assignments go through the rewrite's env peel, the gate reads them exactly as the rewrite does, including where that peel falls short of bash: uppercase names only, and a quoted value not followed by a space (#3262). Both are fixed in that peel itself, where #4136 replaces it, and the gate follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KuSh
force-pushed
the
fix/4281-gate-peel
branch
from
October 7, 2026 00:44
efff29f to
7f36883
Compare
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.
Claude Code matches a deny or ask rule past any leading assignment and past a fixed set of wrappers —
timeout,time,nice,nohup,stdbuf,command,builtin,nogloband a barexargs— soBash(git push *)stopsHUSKY=0 git pushandtimeout 30 git pushas well asgit push. RTK's gate matched the segment as written and nothing else, so every one of those forms fell through to Default; under a host that treats Default as its own decision, as #3909's OpenClaw does, they auto-apply.The rewrite already looked through the same prefixes:
timeout 30 git pushbecametimeout 30 rtk git pushwhile the gate did not recognisegit pushinside it. That was two readings of which command a line runs, and the gap between them is this bug. The deny and ask loops now also trypeel_for_matching, which applies the rewrite's own peels — its env peel, its keyword prefixes, its wrapper table — to a fixpoint, so the gate and the rewrite cannot disagree again. It reads the segment after the split's leftover grammar is gone, since{ HUSKY=0 git push; }reaches the gate as{ HUSKY=0 git push, and the grammar-only reading stays a try of its own for a rule that names the assignment.Two wrappers the host strips were not in the rewrite's table, for a reason that still holds:
stdbufexists to unbuffer the wrapped command's output, which routing through rtk would buffer, andxargsruns whatever it is handed. Recognising a command is not the same decision as rewriting it, so they sit in a match-only table the rewrite never consults.xargsis peeled only in its bare form, as the host does; any option makes the wrapper grammar give up.The line is drawn at the shell.
env,execandnocorrectare peeled too, though the host's list stops short of them; each runs the next word as the command. A program's own arguments are not:git -C . pushstaysgit, as the host documents, since reading past it would mean knowing every program's option grammar.Allow is deliberately left as it was. The host matches allow past known-safe variables only, and peeling a wrapper for it would let
timeout 30 <cmd>inherit<cmd>'s allow rule — so the peel can only make a verdict stricter.Known limits, inherited on purpose. Because assignments go through the rewrite's env peel, the gate reads them exactly as the rewrite does, including where that peel falls short of bash: it takes uppercase names only (
foo=bar rm -rf tmp/is not matched past), and a quoted value not followed by a space is misread (#3262) —D='# git push now', which runs nothing, is denied byBash(git push *). That is the safe direction, and both are fixed in the peel itself where #4136 replaces it with one that follows bash's own assignment rule; the gate follows without a change here. A second, gate-only definition of an assignment would close them sooner, but it is the same kind of drift this PR removes.Test plan
cargo fmt --all && cargo clippy --all-targets && cargo test --all --no-fail-fastondevelope0b2e85a0— 4267 passing, 0 failing, clippy cleanHUSKY=0 nohup timeout 30 git push, empty and quoted values (FOO=,FOO="a b",FOO='a b'), andenv,env FOO=1,exec. Ask fires on three of the same shapes1A=b,=x,FOO-BAR=xand"FOO"=xare commands, not assignments;FOO="x git push y" echo hirunsechoHUSKY=0,timeout 30,niceorenvxargs -n1 git pushandgit -C . push origin mainstay Defaulttrue && { HUSKY=0 git push ; },! timeout 30 git push,( nice git push ); a rule naming the assignment,HUSKY=0 git push *, still matches{ HUSKY=0 git push x ; }For #4281. Independent of the lexing stack (#4130–#4138); when the stack rebases over this,
peel_for_matchingfollows #4136's rename ofstrip_disabled_prefixtosplit_env_prefix.🤖 Generated with Claude Code