Skip to main content
Permission checks for shell tool calls now follow the command’s actual structure, so a deny rule fires even when the blocked command is hidden inside a compound, pipe, subshell, or substitution.
The user approves shell tools; compound commands are parsed so hidden deny rules still block unsafe operations.

Quick Start

1

Block destructive commands — even in compound form

A single deny rule on bash:rm * now catches rm wherever it appears:
Commands like 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.
Single-quote suppression: 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

A deny: 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

When PermissionManager 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:
Protect a config directory from redirects:
CI runner — deny by default, allow git only:

Best Practices

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.
echo '$(rm -rf x)' is correctly not treated as an rm call — the parser respects single-quote suppression. Double-quoted substitutions are still extracted.
Truncating redirects produce a write:<path> sub-target. Use write:/etc/* to block overwrites — bash:cat * alone won’t catch cat foo > /etc/hosts.
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.

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.