Description
In the bash permission evaluator, user deny rules are matched only against the raw command text, while user allow rules are additionally matched against a transparent-wrapper-stripped form. When both rule kinds exist for the same prefix family, a wrapped command can skip the deny and match the allow, so the deny policy is not enforced.
Source evidence
Audited against the 0.4.12 source preview (release/extraction.json sourceRevision 9b9885e42a3cf1a3df1cfa52a46e4fdb034cfcee); line numbers refer to that revision.
- Step 1 (user deny),
packages/agent-modules/permission/src/tools/bash-permission.ts:1136-1145: matches matchShellRule(command, parsedRule) on the raw subcommand only. Step 2 (user ask, lines 1147-1156) is the same.
- Step 5 (user allow),
packages/agent-modules/permission/src/tools/bash-permission.ts:1304-1320: additionally computes stripTransparentWrappersForRuleMatch(command) and matches the stripped form as well. The comment documents the intent: "if a transparent wrapper prefixes the line (nohup/setsid/timeout/xargs/...), also match the stripped form so pnpm:* covers nohup pnpm install".
matchShellRule prefix semantics (packages/agent-modules/permission/src/tools/bash-rule-match.ts:159-171): a prefix rule matches only when the command equals the prefix or starts with prefix + " ".
Worked example with rules allow npm:* and deny npm publish:*:
npm publish → deny rule matches → denied (correct).
nohup npm publish → raw text does not start with npm publish, so the deny rule does not match; the allow rule matches the stripped form npm publish → allowed, bypassing the deny.
Any of the transparent wrappers covered by stripTransparentWrappersForRuleMatch (e.g. nohup, timeout, nice, setsid, xargs) reproduces this.
Expected behavior
Deny rules should see at least every command form that allow rules see. A command that is denied unwrapped should stay denied when wrapped in a transparent wrapper.
Suggested fix
In Step 1 (and Step 2 for ask rules), match matchShellRule(command, parsedRule) || (wrapperStrippedCommand && matchShellRule(wrapperStrippedCommand, parsedRule)) — the same disjunction Step 5 already uses, so deny/ask are evaluated against a superset of the allow forms.
Environment
Found by source audit; not runtime-reproduced yet. No credentials or live systems involved — the example above uses synthetic rules.
Description
In the bash permission evaluator, user deny rules are matched only against the raw command text, while user allow rules are additionally matched against a transparent-wrapper-stripped form. When both rule kinds exist for the same prefix family, a wrapped command can skip the deny and match the allow, so the deny policy is not enforced.
Source evidence
Audited against the 0.4.12 source preview (
release/extraction.jsonsourceRevision9b9885e42a3cf1a3df1cfa52a46e4fdb034cfcee); line numbers refer to that revision.packages/agent-modules/permission/src/tools/bash-permission.ts:1136-1145: matchesmatchShellRule(command, parsedRule)on the raw subcommand only. Step 2 (user ask, lines 1147-1156) is the same.packages/agent-modules/permission/src/tools/bash-permission.ts:1304-1320: additionally computesstripTransparentWrappersForRuleMatch(command)and matches the stripped form as well. The comment documents the intent: "if a transparent wrapper prefixes the line (nohup/setsid/timeout/xargs/...), also match the stripped form sopnpm:*coversnohup pnpm install".matchShellRuleprefix semantics (packages/agent-modules/permission/src/tools/bash-rule-match.ts:159-171): a prefix rule matches only when the command equals the prefix or starts withprefix + " ".Worked example with rules
allow npm:*anddeny npm publish:*:npm publish→ deny rule matches → denied (correct).nohup npm publish→ raw text does not start withnpm publish, so the deny rule does not match; the allow rule matches the stripped formnpm publish→ allowed, bypassing the deny.Any of the transparent wrappers covered by
stripTransparentWrappersForRuleMatch(e.g.nohup,timeout,nice,setsid,xargs) reproduces this.Expected behavior
Deny rules should see at least every command form that allow rules see. A command that is denied unwrapped should stay denied when wrapped in a transparent wrapper.
Suggested fix
In Step 1 (and Step 2 for ask rules), match
matchShellRule(command, parsedRule) || (wrapperStrippedCommand && matchShellRule(wrapperStrippedCommand, parsedRule))— the same disjunction Step 5 already uses, so deny/ask are evaluated against a superset of the allow forms.Environment
Found by source audit; not runtime-reproduced yet. No credentials or live systems involved — the example above uses synthetic rules.