Skip to main content
Interactive approval is now the default for dangerous built-in tools — no configuration needed. When an agent calls a sensitive or external tool, PraisonAI pauses and asks you before it runs. Your choices can be saved as project rules for next time. See Approval for the full list of gated tools and bypass options.
The user requests a change; PraisonAI pauses on dangerous tools until they allow or deny in the terminal.

Quick Start

1

Simple Usage

2

With Configuration

A prompt appears:
Each key chooses how long the approval lasts:
  • [o] once — approve this exact call and prompt again next time (default; backward compatible).
  • [s] session — auto-approve matching calls (same permission target) for the rest of this run. Never persisted — restarting the process starts over.
  • [a] always — persist an allow-rule to approvals.json. The suggested pattern in parentheses (e.g. bash:git status *, edit:src/app.py) is what gets saved; it is auto-derived from the tool + arguments.
  • [n] no — deny (also selected on Ctrl-C / EOF).
The ConsoleBackend keystroke prompt offers no single “allow ALL uses of this tool” key — author a blanket bash:* rule with praisonai permissions allow "bash:*" when you need one. The Rich/Textual TUI used by praisonai code / praisonai chat has a separate Always Allow All option for this — see Rich / Textual TUI.

When approval is required

Approval runs when any of these apply:
  • The agent has approval=True (or a CLI --approval backend)
  • The tool is in the default dangerous-tools list (e.g. bash, write, delete)
  • The tool has trust_level == "external" in the tool registry

Scope choice

Four options in one row — this diagram picks the right one.
A nameless always grant degrades to session. An always grant without an agent_name would match any later agent making the same target call, so the registry refuses to persist it and keeps it in-memory for this run only — which is why an “always” click sometimes doesn’t outlive the run.
session grants are keyed by (agent_name, permission_target) and live in an in-memory set. clear_approved() wipes them, and they are never written to approvals.json.

How the suggested pattern is derived

The always (…) hint isn’t magic — it maps the tool + arguments to a reusable target.
apply_patch stays tool:apply_patch — it takes multi-file patch text, so there is no stable path to scope to. Pressing [a] persists tool:apply_patch, which covers every future apply_patch call regardless of file. If you want per-file scoping, use edit_file instead.

Session-only workflow

Press [s] to auto-approve a repeated call for the rest of the run — nothing is written to disk.
Press [a] instead when the grant should persist across runs.

Change Preview

Before you approve a file-mutating tool, PraisonAI shows a preview so you approve the actual change — not just the tool name. The same preview renders across the ConsoleBackend keystroke prompt, the Rich frontend, and the Textual dialog. For edit / apply_patch, PraisonAI shows a unified diff — either the one the caller supplied (diff / patch argument), or one synthesised on the fly from old_string / new_string (the shape most edit_file calls take):
When the caller supplies only old_string / new_string (the default edit_file shape):
For write (up to 2000 chars, then a \n... (truncated) sentinel is appended), the content is prefixed with the target path:
No preview is shown for read-only or non-file tools.
Preview content is passed through rich.markup.escape, so diffs or new content containing bracket characters ([a-z], [TODO], etc.) render literally and never crash the approval dialog.

Reject with a reason

When you deny a tool call in praisonai code or praisonai chat (Rich mode), the TUI asks for an optional one-line reason. The agent receives that reason as the denied tool’s result and adjusts its next step — no more silent retries.
The agent proposes bash: rm -rf .venv, you deny with a reason, and the agent picks a safer alternative on its next turn.

How it flows

When the prompt appears

Skip the reason (press Enter) when the agent’s proposal is obviously wrong and no correction is worth typing — the plain-denial path is unchanged.
The reason is a plain string returned as the denied tool call’s result — it is not a system prompt, is not persisted to approvals.json, and never affects future approvals. It only informs the agent’s very next decision on this call.

Rich / Textual TUI (praisonai code, praisonai chat)

praisonai code and praisonai chat in Rich or Textual mode render approvals as a scrollable option list (Rich) or a button dialog (Textual) — not the single-key [o]/[s]/[a]/[n] prompt.
When the agent runs git status, the dialog shows the exact pattern each option will persist:
Both Rich and Textual now render the same change preview the ConsoleBackend shows — a unified diff for edit / apply_patch, or truncated content for write — above the approval options, so you approve the concrete change, not just the tool name. Rich draws it as a syntax-highlighted Panel (plain-text fallback when colour is unavailable); Textual shows it inside a scrollable region above the buttons. Non-mutating tools show no preview.
  • Once — approve this exact call, ask again next time.
  • Session Only — reuse the narrow command-scoped pattern in memory for this run only; never written to disk.
  • Always Allow — persist the narrow command-scoped pattern (shell_command:git status * for shell commands; edit:<path> / write:<path> for file tools; a literal single-use target when nothing safe can be derived). It never persists tool:*.
  • Always Allow All — the only option that persists a blanket tool:<tool_name> rule (formerly action_type:*). Pick it deliberately when you truly want every future call of the tool approved.
  • Deny — reject the call, then prompts for an optional one-line reason so the agent knows why and can adjust its next step. Ctrl-C / EOF still counts as a plain denial with no reason.
Approving a single command no longer whitelists every use of that tool. Always Allow narrows to the exact command prefix by default; blanket access is a separate, clearly-labelled Always Allow All entry.
Each option displays its pattern before you choose, so the value shown is exactly the value stored — the Textual ApprovalDialog derives both the narrow and blanket patterns once in __init__ to guarantee they match.
Compound shell commands (&&, |, ;, $(...)) fall back to a literal, single-use target — fail-closed. Approving git status && rm -rf /tmp/x can never be reused as a prefix rule; the stored pattern matches only that exact invocation.
Building a custom TUI on top of InteractiveCore? Use derive_permission_pattern(request, scope=...) in praisonai_code/cli/interactive/events.py. scope="command" returns the narrow pattern (shell → prefix via core derive_pattern, e.g. shell_command:git status *; file tools → concrete path; anything unusable → literal single-use target). scope="tool" is the only path that emits blanket tool:<tool_name>.

Approval modes

The CLI uses --approval bypass but the enum value is bypass_permissions.

Persistence

Press [a] to write an allow-rule to approvals.json via PermissionManager (scoped to the approving agent). The suggested pattern (e.g. bash:git status *, edit:utils.py) is what gets saved: [a] uses the shared suggest_scope_pattern helper so the CLI, YAML --allow/--deny, and Python PermissionManager all scope identically. Compound commands (&&, |, ;, $(...)) fall back to a literal single-use pattern so a persisted rule can only match the exact invocation you approved. For a blanket <tool>:* rule, author it directly with praisonai permissions allow "bash:*". Session grants live in an in-memory _session_scoped_targets set and are cleared by clear_approved() at run teardown. Always grants persist across restarts and survive clear_approved().
Tune the derived pattern with Reusable Approval Scopes — call PermissionManager.suggest_scope_pattern(target) for a custom UI, or hand-author bash:git * via praisonai permissions allow.
Manage rules with:
Each entry carries pattern, approved, scope, created_at, expires_at, agent_name, and a derived flag. derived: true marks approvals whose pattern was auto-generated by reusable command-prefix scopes — user-edited or hand-authored patterns save with derived: false. Old files without the field load cleanly (derived defaults to False).
When building a custom UI or CLI wrapper, call PermissionManager.suggest_scope_pattern(target) to get a derived prefix glob (e.g. bash:git status * for bash:git status -s) before saving a session or always approval. Show the suggestion to the user, let them tweak it, then pass the final value as pattern= to approve(). See Reusable Approval Scopes.

Non-interactive and CI

Without a TTY, prompts default to deny so CI pipelines fail closed.

Best Practices

Use --approval plan until you trust the agent’s behaviour in a codebase.
Tools marked external always prompt — verify third-party integrations before allowing.
Team-wide allow/deny patterns belong in version control.
In both the ConsoleBackend keystroke prompt and the Rich/Textual TUI dialog, the default “Always allow” option persists the narrowest reusable pattern (e.g. shell_command:git status *, edit:src/app.py) — never the blanket action_type:*. It covers every git status … variant but never expands to git push or other subcommands. To allow every future call of the tool, pick the distinct Always Allow All button (Textual) / tool:* menu entry (Rich), or author the rule directly with praisonai permissions allow "shell_command:*".
[s] grants stay in memory and vanish when the process ends — ideal for a repeated call in a single task. Reserve [a] always for calls you want persisted across every future run.
The preview is your last chance to catch an unintended edit. For edit / apply_patch you get a unified diff (either supplied by the caller or synthesised from old_string / new_string). For write you get up to 2000 characters of the new content, prefixed with the target path. Read-only or non-file tools have no preview.
A nameless always grant can’t be persisted — it would match any later agent, so the registry keeps it in-memory for this run only. Set Agent(name=...) so [a] writes a durable, agent-scoped rule.

Bot/chat-channel approvals

This page covers the CLI/terminal approval flow. When running PraisonAI on Telegram, Slack, or Discord, approvals render as interactive buttons and are actor-bound — only the requester and configured admins can resolve them.

Interactive Callback Authorization

Lock approval buttons to specific users in shared chats — covers Telegram, Slack, and Discord bots.
derive_pattern — shared narrow-pattern derivation used by CLI, YAML, and Python rules

Permissions CLI

praisonai permissions reference

Permission Modes

All modes for agents and CLI

Permissions Module

Python SDK API