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.

Windows / PowerShell dialect

PowerShell and cmd wrappers are unwrapped so the inner cmdlet — not the opaque powershell -Command "…" — is what your rules see.

What Windows dialects understand

Recognised mutation vocabulary

Match is case-insensitive; a directory prefix on the executable is ignored. PowerShell (cmdlets + aliases): Remove-Item, New-Item, Move-Item, Copy-Item, Rename-Item, Set-Content, Add-Content, Clear-Content, Out-File, and aliases ri, rd, del, erase, ni, mi, move, cpi, copy, cp, rni, ren, sc, ac, rm, mv. cmd built-ins: del, erase, rd, rmdir, md, mkdir, move, copy, xcopy, ren, rename.
Get-Content, Get-ChildItem, and dir are not mutations — they follow the same read-vs-write distinction as POSIX.

Workspace boundary on Windows

The workspace boundary fires on Windows paths the same way it fires on POSIX paths.
  • The boundary check fires the same way it does for POSIX rm — Windows users no longer get a weaker default.
  • Option-prefixed forms (-NoProfile -Command …, cmd /d /c …) do not defeat the unwrap.
  • -EncodedCommand payloads are decoded (base64 UTF-16LE) before gating, so a base64 blob cannot slip an external write past the gate.
Anchor: Workspace Boundary.

Fail-closed behaviour

  • Undecodable -EncodedCommand → the wrapper stays opaque (executable is preserved as powershell), so a command-specific deny or allow on powershell * still applies; a broad wildcard does not silently authorise the payload.
  • powershell script.ps1 (no command flag) → stays opaque, filename is not treated as a command operand.
  • Any parse failure → whole-command fallback (existing POSIX behaviour, extended to Windows) — no rule is silently weakened.

Which rule should I write?

A rule name matches the executable name: POSIX pattern names catch POSIX executables; Windows cmdlet names catch cmdlets. The path-based workspace boundary is dialect-agnostic, so that half of the safety story needs no per-OS duplication.

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: Windows dialects — with a matching cmdlet deny like bash:Remove-Item * (or bash:del * / bash:New-Item *):

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.
bash:Remove-Item * catches powershell -Command "Remove-Item …", pwsh -c "Remove-Item …", & { Remove-Item … } scriptblocks, and -EncodedCommand payloads that decode to Remove-Item. bash:rm * still catches POSIX rm; write both if your workflow targets both OSes.

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.