deny rule fires even when the blocked command is hidden inside a compound, pipe, subshell, or substitution.
Quick Start
Block destructive commands — even in compound form
A single Commands like
deny rule on bash:rm * now catches rm wherever it appears:cd /tmp && rm -rf x, ls; rm -rf x, and echo $(rm -rf x) are all blocked — not just rm -rf x directly.How It Works
| Step | Action |
|---|---|
| 1 | Tool call arrives as bash:<cmd> target |
| 2 | command_parser.parse_command decomposes into ShellOp list |
| 3 | Each op evaluated as its own sub-target (bash:<exe> <args> and write:<path>) |
| 4 | Original compound target also evaluated (legacy flat-rule compatibility) |
| 5 | Aggregate: deny wins → ask → allow |
What Gets Decomposed
| Operator / Construct | Example | Result | |
|---|---|---|---|
&& (AND) | cd /tmp && rm x | [cd, rm] | |
|| (OR) | ls || rm x | [ls, rm] | |
; (sequence) | ls; rm x | [ls, rm] | |
| (pipe) | cat foo | rm x | [cat, rm] | |
& (background) | rm x & | [rm] | |
Subshell (...) | (cd /tmp && rm x) | [cd, rm] | |
$(...) substitution | echo $(rm -rf x) | [echo, rm] | |
| Backtick substitution | echo `rm -rf x` | [echo, rm] | |
Truncating redirect > | cat foo > /etc/hosts | [cat] + write:/etc/hosts | |
Append redirect >> | echo x >> /etc/hosts | [echo] + write:/etc/hosts | |
| `> | /&>/&>>` | cmd &> /tmp/log | [cmd] + write:/tmp/log |
fd-prefixed 2> / 1>> | ls 2> err.txt | [ls] + write:err.txt | |
| Env-var assignment prefix | FOO=bar rm x | [rm] |
echo '$(rm -rf x)' is a literal string — no rm is extracted.
fd-to-fd redirects like 2>&1 are never treated as write targets.
Input redirects (<, <<, <<<) — the filename is never mistaken for the executable.
Evasions Now Blocked
Adeny: bash:rm * rule now blocks all of these:
| Command | Previous | Now |
|---|---|---|
bash:rm -rf /tmp | denied | denied (unchanged) |
bash:cd /tmp && rm -rf x | ALLOWED | denied |
bash:ls; rm -rf x | ALLOWED | denied |
bash:cat foo | rm x | ALLOWED | denied |
bash:echo $(rm -rf x) | ALLOWED | denied |
bash:echo \rm -rf x“ | ALLOWED | denied |
bash:(cd /tmp && rm -rf x) | ALLOWED | denied |
bash:echo '$(rm -rf x)' (single-quoted) | denied | allowed (correctly — literal) |
Aggregation Precedence
When a compound command produces multiple sub-operations, their results are aggregated as: deny wins → then ask → then allow.| Sub-op results | Aggregate |
|---|---|
| Any deny | deny |
| No deny, any ask | ask |
| All allow | allow |
Fallback Behaviour
For simple single commands (bash:ls -la) with no compound operators, the engine defers to the existing flat matcher — exact backward compatibility. On any parse failure, the whole command is treated as a single op using today’s behaviour, so no existing rule is silently weakened.
Common Patterns
Block all destructive shell ops:Best Practices
Patterns match the executable name, not the full path
Patterns match the executable name, not the full path
bash:rm * catches rm -rf /tmp but not bash:/usr/bin/rm -rf /tmp. Use a regex rule with is_regex: true for absolute-path coverage.Single-quoted substitutions are literals
Single-quoted substitutions are literals
echo '$(rm -rf x)' is correctly not treated as an rm call — the parser respects single-quote suppression. Double-quoted substitutions are still extracted.Protect filesystem locations with write: patterns, not bash: patterns
Protect filesystem locations with write: patterns, not bash: patterns
Truncating redirects produce a
write:<path> sub-target. Use write:/etc/* to block overwrites — bash:cat * alone won’t catch cat foo > /etc/hosts.Zero overhead when permissions are off
Zero overhead when permissions are off
The command parser is lazy-imported: no parsing cost when permissions are not in use or the target is a non-shell tool.
Related
Declarative Permissions
Pre-declare allow/deny rules in YAML, CLI, or Python
Permissions Module
Programmatic PermissionManager API
Permissions CLI
CLI rule management reference
Approval
Interactive approval backends

