deny rule fires even when the blocked command is hidden inside a compound, pipe, subshell, or substitution.
deny rules still block unsafe operations.
Quick Start
1
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.2
Protect files from redirect overwrites
Truncating redirections (
>, >>) emit a write: sub-target. A write: deny rule catches them:cat foo > /etc/hosts and echo x >> /etc/hosts are blocked even though the command starts with cat / echo.How It Works
What Gets Decomposed
The
external_dir: rows above only apply when PermissionManager is created with workspace_root=.... See Workspace Boundary.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:
Aggregation Precedence
When a compound command produces multiple sub-operations, their results are aggregated as: deny wins → then ask → then allow.Workspace Boundary
WhenPermissionManager is created with workspace_root=..., an extra external_dir:<parent>/* sub-target is added for any path that escapes the root — whether it appears as a command argument, a redirect write-target, or an executable path. The same deny→ask→allow aggregation applies: external_dir:* → deny hard-blocks; external_dir:/data/* → allow pre-authorises; the default is ask.
See Workspace Boundary for full details.
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.
Command-aware permissions decompose a compound command; Reusable Approval Scopes generalise a single command’s approval to a whole family.
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
Workspace Boundary
Gate shell/file access outside a project root with
external_dir:*Reusable Approval Scopes
Generalise one approval to cover a whole command family
Command-aware permissions parse inside the command; the workspace boundary gates where on disk it can act.

