Durability: For chat bots, pending approvals can survive a gateway restart when you configure an
ApprovalStore. Each ApprovalRequest includes approval_id, agent_name, and session_id for correlation.Two-layer defence for inbound bots: Gateway Tool Policy runs before dispatch — dangerous tools are never even advertised to the model on untrusted routes. Approval runs after the model decides to call a tool, catching anything that slips through. Use both for defence in depth.
Pruned tool surface (since v1.6.91): Denied tools are removed from the model’s view — both the function schema and the system-prompt enumeration. The model never wastes a turn calling a tool it cannot execute. See How tools are pruned from the LLM.
Earlier change (PR #2122): Approval is enabled by default. YAML configs that omitted the
approval key previously got enabled: false; they now get the prompting policy.Sync and Async Parity: Approval checks now work uniformly in both sync and async tool execution paths.
Quick Start
Default (safe, no setup needed)
Dangerous tools are gated automatically. Just run your agent — it will ask before doing anything destructive:
Deny silently (no prompts)
Block dangerous tools without prompting (useful for CI that should fail fast):
What counts as dangerous
TheDEFAULT_DANGEROUS_TOOLS set (from praisonaiagents/approval/registry.py) determines which tools trigger approval:
| Tool name | Risk level | Description |
|---|---|---|
execute_command | critical | Runs arbitrary shell commands |
kill_process | critical | Terminates running processes |
execute_code | critical | Executes arbitrary code |
acp_execute_command | critical | ACP shell command execution |
write_file | high | Writes / creates a file |
delete_file | high | Deletes a file |
move_file | high | Moves / renames a file |
copy_file | high | Copies a file |
acp_create_file | high | ACP file creation |
acp_edit_file | high | ACP file edits |
acp_delete_file | high | ACP file deletion |
execute_query | high | Executes a database query |
evaluate | medium | Evaluates code expressions |
crawl | medium | Crawls web URLs |
scrape_page | medium | Scrapes a web page |
Interactive vs non-interactive
PraisonAI checks whether bothstdin and stdout are TTYs to decide what to do when no approval= argument is passed:
| Context | Result |
|---|---|
| Terminal (TTY) | Ask — ConsoleBackend prompts before each dangerous tool |
| CI / piped / script | Deny — default permission preset blocks destructive ops |
default preset specifically blocks: execute_command, kill_process, execute_code, acp_execute_command, delete_file, move_file, copy_file, acp_delete_file. Write and create operations (write_file, acp_create_file, acp_edit_file) still run — they are blocked only under the safe or read_only presets.
How tools are pruned from the LLM
Denied tools are filtered out of both the function schema and the system prompt before the LLM ever sees them.| Layer | Before v1.6.91 | Since v1.6.91 |
|---|---|---|
| Function schema sent to LLM | All tools advertised; denial only at execution | Denied tools removed from the schema |
"You have access to the following tools: …" in system prompt | All tool names listed | Only allowed tool names listed |
ask and allow tools stay advertised — approval still runs at execution time as defence in depth. Only tools whose permission tier resolves to a hard deny (preset deny set, or explicit *: deny rule) disappear from the model’s view.| Preset | Pruned from LLM? | Use when |
|---|---|---|
approval="full" / "bypass" | No — everything visible | You fully trust the environment |
approval="default" (auto-applied in CI / non-TTY) | Yes — shell exec + destructive file ops removed | Default safety, write-friendly |
approval="safe" / "read_only" | Yes — all dangerous tools removed | Read-only / review agents |
approval={"permissions": {"*": "deny", ...}} | Yes — anything matched as deny removed | Custom allow-lists |
approval={"permissions": {"bash:rm *": "deny", ...}} | Yes — pattern-matched deny rules removed (native + MCP) | Fine-grained rule-based safety |
Pattern-based rules and MCP tools
The same pruning path now covers pattern-based rules — not just presets. Rules loaded from.praisonai/permissions/, YAML, CLI flags, or a PermissionManager are consulted via PermissionManager.is_denied() when the schema is built, and MCP tools go through the identical gate.
tool:<name> prefix match rules written against either the bare name or the prefixed form.
Bypassing safety
Three ways to restore unrestricted behaviour: 1. CLI flagoff, full, none, 0, false.
3. Python / YAML
How users approve
On the console backend, the prompt now offers four choices:
[o] once, [s] this session, [a] always, [n] no. See Interactive Tool Approval for the full scope semantics.Configuration
| Option | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Turn approval on/off — safe by default |
backend | str | "console" | One of: console, slack, telegram, discord, webhook, http, agent, auto, none |
approve_all_tools | bool | false | If true, every tool needs approval (not just risky ones) |
timeout | float | null | Seconds to wait for a decision; null = no timeout |
approve_level | ApprovalLevel | null | null | Auto-approve up to this risk level: low, medium, high, critical |
guardrails | str | null | Free-text guardrail description |
default_policy | "deny" | "prompt" | "allow" | "prompt" | Policy when no per-tool entry matches |
approve_tools | Dict[str, ApprovalLevel] | null | null | Per-tool granularity, e.g. {"shell": "critical"} |
permissions | Dict[str, Any] | null | Declarative allow/deny/ask rules. See Declarative Permissions. |
Approval backends decide how to ask a human (Slack, console, …). Declarative
permissions decide whether to ask at all in non-interactive runs.YAML
Hook installation
Register abefore_tool hook that enforces the policy:
approval: true (console), approval: slack (named backend), approval: false / null (off).
Unknown keys raiseValueError— typos likeapprove_levels:will fail loudly.
CLI
| CLI flag | YAML / Python field |
|---|---|
--trust | backend: auto |
--approval <name> | backend: <name> |
--approve-all-tools | approve_all_tools: true |
--approval-timeout <s> | timeout: <s> |
--approve-level <l> | approve_level: <l> |
--allow <pattern> | permissions: { "<pattern>": allow } |
--deny <pattern> | permissions: { "<pattern>": deny } |
--permissions <file> | Load rules from YAML/JSON file |
--permission-default <action> | permissions: { "*": <action> } |
--guardrail "<txt>" | guardrails: "<txt>" |
Using approval with async agents
When using async agents (.achat(), .astart(), or async tools), the default console backend will fail with PermissionError. Configure a non-console backend:
webhook, http, slack, telegram, discord, agent.
Argument-aware approval cache
Approval grants are scoped to the exact tool arguments — calling the same tool with different arguments triggers a fresh approval — unless you approve withreusable_scope=True, which stores a derived prefix pattern that covers arg variants. See Reusable Approval Scopes.
Persistent shell approvals (
scope="always" / "session") can opt into a reusable command-prefix scope: approving bash:git status -s records the pattern bash:git status * and covers all trailing-arg variants of the same subcommand. See Reusable command-prefix approvals. Compound commands (&&, |, ;, $()) and bare commands with no subcommand stay literal. Interactive users reach the same scopes through the [s] session and [a] always keys in the console prompt.write_file({"path": "/tmp/a.txt"}) and write_file({"path": "/tmp/b.txt"}) produce different keys and each requires its own approval.
Critical-risk tools (execute_command, kill_process, execute_code) always re-prompt regardless of the cache — is_already_approved returns False unconditionally for them.
For persistent approvals across sessions, see Reusable Approval Scopes — once PraisonAI PR #2576 is merged, the pattern will be auto-derived from a command-arity table so git status -s and git status --short share one rule (bash:git status *).
Worked example:
This is a behavior change from previous versions where approving a tool name once would auto-approve all subsequent calls to that tool in the same context, regardless of arguments. Now only calls with identical arguments skip the prompt.
Durable, Per-Agent Scoping
On the gateway, “allow always” grants persist across restart and default to being scoped to the approving agent — one agent’s approval no longer authorises every other agent. Grants live in a SQLite store at~/.praisonai/state/gateway/approvals.sqlite and expire after 90 days by default.
Gateway Scoped Approvals
Durable, agent-scoped allow-always grants, the
scope_to_agent / scope_to_args resolver options, and the /api/approval/allow-list endpointTroubleshooting
| Error | Cause | Fix |
|---|---|---|
PermissionError: Approval request failed for <tool> | Async agent with console backend | Configure a non-console backend |
RuntimeError: Tool '<tool>' requires approval but cannot use console I/O from async context. | Same root cause, surfaced earlier | Same fix |
| Tool I expected to be called was never tried by the model | Tool name is in the active deny set / preset and is pruned from the LLM’s view | Either widen permissions (e.g. approval="full") or change your permissions rules; check the agent’s _perm_deny at runtime |
Best Practices
Console approval
Console approval
No env vars needed; you see the prompt directly in the terminal. This is now the default when running in an interactive session.
Slack approval
Slack approval
Routes approval requests to a channel humans already watch — great for CI pipelines that need human gating.
Set timeouts
Set timeouts
Without a timeout, the agent blocks indefinitely waiting for a decision.
Use approve_level
Use approve_level
approve_level: high lets safe tools run without prompts and only gates the dangerous ones.Restore old behavior for trusted environments
Restore old behavior for trusted environments
Use
approval="bypass" or PRAISONAI_TOOL_SAFETY=off when you control the environment fully and want the pre-4.6.27 unrestricted behaviour.Use approval='safe' for review and plan agents
Use approval='safe' for review and plan agents
Use
approval="safe" or approval="read_only" for review/plan agents — the model is offered only read tools, so it can’t waste turns calling write or shell tools that would just be denied.Related
All backend protocols (Slack, Telegram, Discord, Webhook, HTTP, Agent)
Full CLI flag reference
Interactive terminal approval experience
Restart-safe pending approvals for bots
Permission modes (plan, accept-edits, bypass)
Screen/mouse/keyboard control — canonical per-action approval-callback example

