Skip to content

fix(permissions): match deny and ask rules past assignments and wrappers (#4281) - #4284

Open
KuSh wants to merge 1 commit into
developfrom
fix/4281-gate-peel
Open

KuSh wants to merge 1 commit into
developfrom
fix/4281-gate-peel

Conversation

@KuSh

@KuSh KuSh commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

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.

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 by Bash(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-fast on develop e0b2e85a0 — 4267 passing, 0 failing, clippy clean
  • Every prefix and assignment row was run through bash with a stub for the command first, so each expected verdict is bash's
  • Deny fires past the issue's twelve rows, a stacked HUSKY=0 nohup timeout 30 git push, empty and quoted values (FOO=, FOO="a b", FOO='a b'), and env, env FOO=1, exec. Ask fires on three of the same shapes
  • Deny does not fire where bash runs something else: 1A=b, =x, FOO-BAR=x and "FOO"=x are commands, not assignments; FOO="x git push y" echo hi runs echo
  • Allow is not widened through HUSKY=0, timeout 30, nice or env
  • xargs -n1 git push and git -C . push origin main stay Default
  • Grammar and a prefix together are matched past: true && { 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 ; }
  • Mutation — seven, each compiled and each caught: the env peel dropped from the gate; the match-only wrappers emptied; the peel removed from the deny loop alone; from the ask loop alone; the peel reading the raw segment instead of the grammar-stripped one; the grammar-only reading dropped; the peel added to allow

For #4281. Independent of the lexing stack (#4130–#4138); when the stack rebases over this, peel_for_matching follows #4136's rename of strip_disabled_prefix to split_env_prefix.

🤖 Generated with Claude Code

@rtk-wshm-sync-bot rtk-wshm-sync-bot Bot added bug Something isn't working permissions security labels Sep 26, 2026
@rtk-wshm-sync-bot

Copy link
Copy Markdown

wshm · Automated triage by AI

📊 Automated PR Analysis

🐛 Type bug-fix
🟡 Risk medium

Summary

Fixes 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

  • Tests present
  • Breaking change
  • Docs updated

Linked issues: #4281


Analyzed automatically by wshm · This is an automated analysis, not a human review.

@KuSh
KuSh marked this pull request as ready for review September 26, 2026 18:03
@KuSh
KuSh force-pushed the fix/4281-gate-peel branch 2 times, most recently from c8b1821 to c9a3f6e Compare September 26, 2026 21:22
@KuSh
KuSh force-pushed the fix/4281-gate-peel branch from c9a3f6e to efff29f Compare October 5, 2026 21:22
…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
KuSh force-pushed the fix/4281-gate-peel branch from efff29f to 7f36883 Compare October 7, 2026 00:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working permissions security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant