Tool Approval
Control tool execution approval in PraisonAI CLI
PraisonAI asks before running anything dangerous: a fresh install on an interactive terminal will prompt you the first time a tool wants to touch your files or shell.
Default Behaviour
PraisonAI is safe by default: a fresh install on an interactive terminal asks before running dangerous tools (shell exec, file writes, code execution). Pipes, CI runners, and redirected I/O fall back to deny by default — dangerous tools never run unattended without an explicit policy.
| Context | Default | What you see |
|---|---|---|
| Interactive terminal (TTY) | Ask | An approval prompt appears the first time a dangerous tool is called |
| Pipe / CI / non-TTY | Deny | The dangerous tool is hard-denied before it runs |
PRAISONAI_TOOL_SAFETY=off set | Bypass | Legacy ungated behaviour; nothing is gated |
approval=False (Python) | Deny | Same as non-TTY: deny-by-default, no prompt |
approval="bypass" (Python) | Bypass | Same as PRAISONAI_TOOL_SAFETY=off |
from praisonaiagents import Agent
# Safe-by-default: on a TTY, asks; off-TTY, denies — no kwargs needed
agent = Agent(name="Dev Agent", instructions="Help with file ops",
tools=["write_file", "execute_command"])
agent.start("create a hello.txt file")A session counts as interactive only when both sys.stdin and sys.stdout are TTYs. This avoids surprises in CI / Docker / scheduled-task contexts.
Reject with a Reason
When you reject a tool call, PraisonAI can capture a one-line explanation and hand it back to the agent as the tool’s result, so the next turn course-corrects instead of retrying blindly. On the terminal ConsoleBackend, pick [d] deny & redirect to open the What should the agent do instead? follow-up; the Rich prompt and Textual TUI ask for an optional reason after Reject.
Allow execute_command? [o] once [s] this session [a] always (bash:git status *) [n] no [d] deny & redirect
Choice [o/s/a/n/d] (n): d
What should the agent do instead?: don't touch production.env — use staging.env insteadLeave the follow-up prompt blank to reject without guidance — the plain-denial behaviour is unchanged.
| Frontend | Reject prompt | Notes |
|---|---|---|
| Rich (default terminal) | Asks for an optional reason after choosing Reject | Reason threaded into the reject response |
| Textual (TUI) | Reject dialog takes an optional reason | Reason threaded into the reject response |
| Console backend | First-class [d] deny & redirect key that opens a second prompt | Deny and steer in one keystroke |
On the console path, the guidance travels as ApprovalDecision.feedback (a new Optional[str] field on ApprovalDecision, defaulting to None). When present, tool_execution.py rewrites the denied tool’s error_msg to "Tool '<name>' was denied by the user. Do not retry it as-is. User guidance: <feedback>", so the agent’s next turn sees the correction as the tool’s result — not just a log line. The Rich/Textual TUI still surfaces the same value through ApprovalResponse.reason and the praisonai_code wrapper (InteractiveCLIApprovalBackend) now sets feedback on the returned decision as well, keeping the two paths in sync.
Bypassing the Prompt
Per-run bypass — --dangerously-skip-approval
For one-off runs you trust completely:
praisonai code "rename src/ to lib/" --dangerously-skip-approvalThis is identical to --no-safe. Both do two things:
- Set env vars —
PRAISON_APPROVAL_MODE=auto,PRAISONAI_TOOL_SAFETY=off— consumed by the CLI’s own tool wiring. - Register
AutoApproveBackendon the approval registry — consumed by the core@require_approvaldecorator that gates critical tools likeacp_execute_command. Without this the flag was a no-op for any registry-decorated tool, so a scripted run with no TTY auto-denied every prompt while still exiting0.
A safe-mode run removes the AutoApproveBackend a prior --no-safe installed in the same process (REPL/worker/test), and removes only that backend — a caller-supplied backend or one installed by --plan is preserved. A per-agent backend still wins (the core consults Agent(approval=…) first), and --plan remains mutually exclusive with --no-safe / --dangerously-skip-approval (exits 1).
Session-wide bypass — PRAISONAI_TOOL_SAFETY=off
For a whole shell session:
export PRAISONAI_TOOL_SAFETY=off
praisonai code "..." # all calls in this shell skip the safe-default askAccepted values that bypass: off, full, none, 0, false (case-insensitive). Any other value falls back to the default preset.
Python-level bypass — approval="bypass"
from praisonaiagents import Agent
agent = Agent(name="Dev Agent", approval="bypass", tools=["write_file", "execute_command"])
agent.start("task")Use approval=False instead if you want deny-by-default without the prompt (CI-style behaviour from inside a TTY-attached process).
Declarative Permissions
Pre-declare allow/deny rules for CI-safe runs. See Declarative Permissions.
praisonai run agents.yaml \
--allow 'read:*' \
--allow 'bash:git *' \
--deny 'bash:rm *' \
--permission-default ask| Flag | Description |
|---|---|
--allow <pattern> | Allow matching tool calls (repeatable) |
--deny <pattern> | Deny matching tool calls (repeatable) |
--permissions <file> | Load rules from YAML or JSON |
--permission-default <action> | Default for unmatched patterns (allow, deny, ask) |
Overview
Certain tools are marked with risk levels and require approval before execution:
| Risk Level | Tools | Default Behavior |
|---|---|---|
| CRITICAL | execute_command, kill_process, execute_code | Always prompts |
| HIGH | write_file, delete_file, move_file, copy_file, edit_file, apply_patch | Always prompts |
| MEDIUM | evaluate, crawl, scrape_page | Always prompts |
| LOW | (none by default) | Always prompts |
YAML Configuration
Configure approval settings in your YAML configuration file using the approval: block:
permissions:
"read:*": allow
"bash:rm *": deny
approval:
enabled: true # bool, default false
backend: "console" # console | slack | telegram | discord | webhook | http | agent | auto | none
approve_all_tools: false # bool, default false
timeout: 60 # float seconds, default null (no timeout); accepts "none"
approve_level: "high" # low | medium | high | critical, default null
guardrails: "..." # optional guardrail descriptionShorthand Forms
approval: true # → enabled, console backend
approval: false # → disabled
approval: slack # → enabled, slack backend
approval: null # → disabledLegacy YAML Aliases (Backward Compatible)
| Legacy key | New key |
|---|---|
backend_name | backend |
all_tools | approve_all_tools |
approval_timeout | timeout |
Primary keys win when both are present.
Validation
Unknown keys now raise ValueError with the list of allowed keys — silent typos no longer pass through undetected.
CLI ⇄ YAML ⇄ Python Mapping
All three surfaces use the same unified configuration:
| CLI flag | YAML key | Python field |
|---|---|---|
--trust | N/A (use backend: auto) | backend="auto", enabled=True |
--approval <backend> | backend: <backend> | backend="<backend>", enabled=True |
--approve-all-tools | approve_all_tools: true | approve_all_tools=True |
--approval-timeout <sec> | timeout: <sec> | timeout=<sec> |
--approve-level <level> | approve_level: <level> | approve_level="<level>" |
--guardrail "<text>" | guardrails: "<text>" | guardrails="<text>" |
praisonai code Safety Flags
| Flag | Default | Effect |
|---|---|---|
--safe / --no-safe | --safe (ON) | Safe mode ON by default. --no-safe sets PRAISONAI_TOOL_SAFETY=off and restores legacy ungated behaviour. |
--dangerously-skip-approval | OFF | Skip all approval prompts and run dangerous tools unguarded. Equivalent to --no-safe. |
--plan | OFF | Read-only — every mutating tool is denied. Discoverable alias for --approval plan (PermissionMode.PLAN). Cannot be combined with --no-safe / --dangerously-skip-approval (fails closed). |
--trust | OFF | Auto-approve all tool executions. |
--approval <backend> | — | Route approvals to a backend (console, slack, …). |
--plan is the opposite of --dangerously-skip-approval — it denies the mutating class of tools outright rather than approving them all. See Permission Modes.
What safe mode does
praisonai code --safe writes PRAISON_APPROVAL_MODE=prompt. The CLI’s tool loader accepts this value (plus auto / manual / scoped); any other string is logged and coerced to prompt. The in-wrapper runtime still orchestrates ACP plans in auto, but the core @require_approval gate on each tool is the sole approval point — it prompts on a TTY and denies without one. edit_file / apply_patch only get a standing approval when auto is combined with an explicit edit-group request.
Auto-Approve All Tools (--trust)
Use the --trust flag to auto-approve all tool executions without prompting:
praisonai "hello world" --trust
Usage
# Auto-approve all tools (use with caution!)
praisonai "run ls -la command" --trustOutput:
⚠️ Trust mode enabled - all tool executions will be auto-approved
Tools used: execute_command
[directory listing...]Level-Based Approval (--approve-level)
Use --approve-level to auto-approve tools up to a specific risk level:
# Auto-approve low, medium, and high risk tools
# Still prompt for critical tools
praisonai "write to file and run command" --approve-level highAvailable levels:
low– Only auto-approve low risk toolsmedium– Auto-approve low and medium risk toolshigh– Auto-approve low, medium, and high risk toolscritical– Auto-approve all tools (same as--trust)
Examples
# Auto-approve only low risk tools
praisonai "task" --approve-level low
# Auto-approve up to medium risk
praisonai "task" --approve-level medium
# Auto-approve up to high risk (prompt for critical)
praisonai "task" --approve-level high
# Auto-approve everything (same as --trust)
praisonai "task" --approve-level criticalApproval Backend (--approval)
Use --approval to route tool approvals to a specific backend — Slack, Telegram, Discord, a webhook, or more:
# Route approvals to Slack
praisonai "deploy to production" --approval slack
# Route approvals to Telegram
praisonai "delete old logs" --approval telegram
# Auto-approve everything (same as --trust)
praisonai "run tests" --approval auto
# Interactive console prompt (default)
praisonai "clean up files" --approval consoleAvailable Backends
| Value | Backend | Required Env Vars |
|---|---|---|
console | Interactive terminal prompt (default) — see Interactive Tool Approval for the once/session/always scope choices | — |
slack | Slack Block Kit message + reply polling | SLACK_BOT_TOKEN, SLACK_CHANNEL |
telegram | Telegram inline keyboard + polling | TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID |
discord | Discord embed + text reply polling | DISCORD_BOT_TOKEN, DISCORD_CHANNEL_ID |
webhook | POST to HTTP endpoint + poll for decision | APPROVAL_WEBHOOK_URL |
http | Local web dashboard (browser-based) | — |
agent | Delegate to an AI reviewer agent | — |
auto | Auto-approve all (same as --trust) | — |
none | Disable approval entirely | — |
Works With All CLI Commands
# Direct prompt
praisonai "task" --approval slack
# Run command
praisonai run "task" --approval telegram
# Chat / TUI
praisonai chat --approval discordProgrammatic Control
from praisonaiagents import Agent
# Safe-by-default (TTY = ask, non-TTY = deny)
agent = Agent(name="Dev Agent", tools=["write_file", "execute_command"])
# Deny-by-default without any prompt (CI-style)
agent = Agent(name="CI Agent", approval=False, tools=["write_file"])
# Full bypass — legacy ungated behaviour
agent = Agent(name="Trusted Agent", approval="bypass", tools=["write_file"])Custom approval callbacks:
from praisonaiagents.approval import (
set_approval_callback,
ApprovalDecision,
remove_approval_requirement,
add_approval_requirement
)
# Auto-approve all tools
def auto_approve(function_name, arguments, risk_level):
return ApprovalDecision(approved=True, reason="Auto-approved")
set_approval_callback(auto_approve)
# Level-based approval
def level_approve(function_name, arguments, risk_level):
levels = {"low": 1, "medium": 2, "high": 3, "critical": 4}
if levels.get(risk_level, 4) <= levels["high"]:
return ApprovalDecision(approved=True)
return ApprovalDecision(approved=False, reason="Too risky")
set_approval_callback(level_approve)
# Remove approval requirement for specific tool
remove_approval_requirement("execute_command")
# Add approval requirement for custom tool
# Custom tools registered here are also honoured on the interactive ask path (ConsoleBackend)
add_approval_requirement("my_dangerous_tool", risk_level="high")Risk Level Reference
Critical Risk Tools
execute_command– Run shell commandskill_process– Terminate processesexecute_code– Execute arbitrary code
High Risk Tools
write_file– Write to filesdelete_file– Delete filesmove_file– Move/rename filescopy_file– Copy filesedit_file– Edit existing file contentapply_patch– Apply a patch/diff to filesexecute_query– Database queries
Medium Risk Tools
evaluate– Evaluate expressionscrawl– Web crawlingscrape_page– Web scraping
Best Practices
Use deny-by-default in CI
Set approval=False (or let the non-TTY default kick in automatically) so an agent run in CI never silently executes a destructive tool, even on a misconfigured runner that mistakenly attaches a TTY.
from praisonaiagents import Agent
# Explicit deny-by-default for CI pipelines
agent = Agent(name="CI Agent", approval=False, tools=["write_file", "execute_command"])
agent.start("run tests and update report")Never set PRAISONAI_TOOL_SAFETY=off in a shared shell profile
PRAISONAI_TOOL_SAFETY=off is a per-session bypass intended for one-off trusted runs. Putting it in ~/.bashrc or ~/.zshrc re-introduces every safety gap that the safe-by-default system closed — any agent running in that shell will execute dangerous tools unguarded.
For development/testing: use –trust for faster iteration
When you’re actively monitoring the AI’s actions, --trust speeds up iteration by skipping prompts entirely.
praisonai "refactor utils.py" --trustFor production scripts: use –approve-level high
Allow file operations but still require approval for shell commands:
praisonai "generate and save report" --approve-level high