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.
Argument-scoped deny rules now reach the enforcement path (PraisonAI PR #4234, closes #4228): On releases prior to #4234, an argument-scoped deny rule like bash:rm * on a PermissionManager was parsed and understood by the rule engine, but the name-based deny gate only asked is_denied(function_name). So if execute_command was allowed by name, execute_command(command="rm -rf /tmp/x") still ran — the argument-scoped rule never fired at call time. After #4234 the gate builds a scoped target from the arguments and checks both, so the rule fires. Upgrade to a release that includes #4234 — no code change is required.
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.

Shell expansion escalates to ASK

Shell commands containing an expansion that cannot be statically resolved are escalated to ASK instead of being silently allowed by a broad rule. Before the tokenizer runs, the manager checks for $IFS, ${VAR}, and bare $VAR. If any is present, an explicit deny still wins; otherwise the request is escalated to ASK with the reason “Command contains shell expansion that cannot be statically verified; requires approval”. Command substitution ($(...) and backticks) is excluded — it is already decomposed per-op, so its inner commands keep matching deny rules.
Before this change, a broad bash:* allow could shadow a specific bash:rm * deny for anything containing ${IFS} or $HOME — a real permission bypass.

Evasions Now Blocked

A deny: bash:rm * rule now blocks all of these:

Argument-scoped deny reaches the gate

The rule engine has always understood argument-scoped patterns like bash:rm *. Since PR #4234 the enforcement path does too — the deny gate builds a scoped target from the call’s arguments and checks it alongside the tool name.
Name-level rules keep their all-or-nothing behaviour. execute_command: deny still blocks every execute_command(...) call regardless of arguments. The scoped check is additive — it narrows a name that is otherwise allowed; it never loosens a name-level deny.
Argument-key mangling can’t evade the gate. A malformed {"command=": "rm -rf /tmp/x"} (a trailing-= kwarg LLMs sometimes hallucinate) is normalised in the gate with k.strip().rstrip('=').strip() before the scoped target is built — so a deny rule can’t be dodged by a key that only cleans up after dispatch.

Modified-args re-authorisation

An approval backend may rewrite arguments (a common sanitisation pattern). Since PR #4234, the rewritten args are re-checked against the deny gate before dispatch — so a rewrite can’t smuggle a denied command past a gate that only saw the original, safe args.
Both the sync and async approval paths re-run the gate on the final arguments. BYPASS mode opts out entirely, matching its “skip all permission checks” contract.

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.