Skip to main content
Approval pauses an agent before it runs a risky tool and asks a human (or another channel) to allow or deny it.
from praisonaiagents import Agent

agent = Agent(
    name="Coder",
    instructions="Refactor safely",
    tools=["shell", "write_file"],
)
agent.start("Remove unused imports in utils.py")
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.
Behaviour change (PR #2369): Dangerous built-in tools (execute_command, kill_process, execute_code, delete_file, move_file, copy_file, and others) are now gated by default. Interactive sessions (TTY) ask before running; non-interactive sessions (CI/pipes) deny. Read-only tools are unaffected. See What counts as dangerous below.
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.
The user requests a code change; the agent pauses for approval before running dangerous tools.

Quick Start

1

Default (safe, no setup needed)

Dangerous tools are gated automatically. Just run your agent — it will ask before doing anything destructive:
from praisonaiagents import Agent

agent = Agent(
    name="Coder",
    instructions="Refactor utils.py",
    tools=["shell", "write_file"],
    # approval is automatic: asks on a TTY, denies in CI
)
agent.start()
2

Bypass safety (opt-out)

To restore the old unrestricted behaviour:
from praisonaiagents import Agent

agent = Agent(
    name="Admin",
    instructions="Manage the server",
    approval="bypass",   # run dangerous tools silently
)
agent.start("Clean up old logs")
Or via environment variable:
PRAISONAI_TOOL_SAFETY=off praisonai code
3

Deny silently (no prompts)

Block dangerous tools without prompting (useful for CI that should fail fast):
agent = Agent(
    name="Reviewer",
    instructions="Review code only",
    approval=False,    # deny dangerous tools silently
)
4

Full Configuration

agent = Agent(
    name="Admin",
    instructions="...",
    approval={
        "enabled": True,
        "backend": "slack",
        "approve_all_tools": False,
        "timeout": 120,
        "approve_level": "high",
    },
)

What counts as dangerous

The DEFAULT_DANGEROUS_TOOLS set (from praisonaiagents/approval/registry.py) determines which tools trigger approval:
Tool nameRisk levelDescription
execute_commandcriticalRuns arbitrary shell commands
kill_processcriticalTerminates running processes
execute_codecriticalExecutes arbitrary code
acp_execute_commandcriticalACP shell command execution
write_filehighWrites / creates a file
delete_filehighDeletes a file
move_filehighMoves / renames a file
copy_filehighCopies a file
acp_create_filehighACP file creation
acp_edit_filehighACP file edits
acp_delete_filehighACP file deletion
execute_queryhighExecutes a database query
evaluatemediumEvaluates code expressions
crawlmediumCrawls web URLs
scrape_pagemediumScrapes a web page
Read-only tools (search, read_file, etc.) are not in this set and run without gating.

Interactive vs non-interactive

PraisonAI checks whether both stdin and stdout are TTYs to decide what to do when no approval= argument is passed:
ContextResult
Terminal (TTY)AskConsoleBackend prompts before each dangerous tool
CI / piped / scriptDenydefault permission preset blocks destructive ops
The 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.
LayerBefore v1.6.91Since v1.6.91
Function schema sent to LLMAll tools advertised; denial only at executionDenied tools removed from the schema
"You have access to the following tools: …" in system promptAll tool names listedOnly allowed tool names listed
from praisonaiagents import Agent

def execute_command(command: str) -> str:
    """Run a shell command."""
    ...

def read_file(path: str) -> str:
    """Read a file."""
    ...

agent = Agent(
    name="Reader",
    instructions="Help me explore the codebase",
    tools=[execute_command, read_file],
    approval="safe",   # blocks execute_command (dangerous)
)

agent.start("Show me what's in main.py")
# The model is offered ONLY read_file.
# execute_command is invisible — no denied-call loops, no wasted turns.
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.
PresetPruned from LLM?Use when
approval="full" / "bypass"No — everything visibleYou fully trust the environment
approval="default" (auto-applied in CI / non-TTY)Yes — shell exec + destructive file ops removedDefault safety, write-friendly
approval="safe" / "read_only"Yes — all dangerous tools removedRead-only / review agents
approval={"permissions": {"*": "deny", ...}}Yes — anything matched as deny removedCustom 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.
from praisonaiagents import Agent
from praisonaiagents.mcp import MCP

agent = Agent(
    name="Ops",
    instructions="Run ops tasks",
    tools=MCP("http://localhost:8000/sse"),
    approval={
        "permissions": {
            "tool:delete_*": "deny",   # MCP-namespaced tool names
        },
    },
)
# delete_* MCP tools never appear in the schema and are blocked at call time
# with {"permission_denied": True}.
MCP tools using a tool:<name> prefix match rules written against either the bare name or the prefixed form.
When you set approval="safe" on a code-review agent and notice the model never tries to edit or run shell — that’s pruning working. No prompts appear because the model isn’t asking.

Bypassing safety

Three ways to restore unrestricted behaviour: 1. CLI flag
praisonai code --dangerously-skip-approval
# also sets PRAISONAI_TOOL_SAFETY=off for any subprocess tree
2. Environment variable
PRAISONAI_TOOL_SAFETY=off praisonai code
Accepted “off” values: off, full, none, 0, false. 3. Python / YAML
from praisonaiagents import Agent

Agent(tools=["shell", "write_file"], approval="bypass")   # silent allow
Agent(tools=["shell", "write_file"], approval=False)       # silent deny (no prompts)
Agent(tools=["shell", "write_file"], approval="safe")      # block all dangerous tools
Agent(tools=["shell", "write_file"], approval="read_only") # alias of "safe"
Agent(tools=["shell", "write_file"], approval="full")      # no restrictions

How users approve

Configuration

The default console backend requires a sync call stack. If you decorate a tool with @require_approval and invoke it from an async agent (achat, astart, async tools, async callbacks), the call now raises PermissionError. Configure a non-console backend (HTTP, Slack, webhook) or drive the agent from sync code.
OptionTypeDefaultDescription
enabledbooltrueTurn approval on/off — safe by default
backendstr"console"One of: console, slack, telegram, discord, webhook, http, agent, auto, none
approve_all_toolsboolfalseIf true, every tool needs approval (not just risky ones)
timeoutfloatnullSeconds to wait for a decision; null = no timeout
approve_levelApprovalLevel | nullnullAuto-approve up to this risk level: low, medium, high, critical
guardrailsstrnullFree-text guardrail description
default_policy"deny" | "prompt" | "allow""prompt"Policy when no per-tool entry matches
approve_toolsDict[str, ApprovalLevel] | nullnullPer-tool granularity, e.g. {"shell": "critical"}
permissionsDict[str, Any]nullDeclarative 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

agents:
  admin:
    role: Server admin
    approval:
      enabled: true
      backend: slack
      timeout: 120
      approve_level: high
      default_policy: prompt
      approve_tools:
        shell: critical
        read_file: low

Hook installation

Register a before_tool hook that enforces the policy:
from praisonai._approval_spec import ApprovalSpec

spec = ApprovalSpec(
    enabled=True,
    default_policy="prompt",
    approve_tools={"shell": "critical"},
)
spec.install_hook()
Shorthands: approval: true (console), approval: slack (named backend), approval: false / null (off).
Unknown keys raise ValueError — typos like approve_levels: will fail loudly.

CLI

praisonai "deploy" --approval slack --approval-timeout 120 --approve-level high
CLI flagYAML / Python field
--trustbackend: auto
--approval <name>backend: <name>
--approve-all-toolsapprove_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:
from praisonaiagents import Agent
from praisonaiagents.approval import get_approval_registry, WebhookBackend

# Configure webhook backend for async compatibility
get_approval_registry().set_backend(
    WebhookBackend(url="http://localhost:8080/approve")
)

agent = Agent(
    name="AsyncBot",
    instructions="Process requests asynchronously",
    approval=True
)

# This now works with async agents
await agent.astart("Delete old files")
Available non-console backends: 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 with reusable_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.
The cache key formula is:
"{tool_name}:{sha256(json.dumps(arguments, sort_keys=True))[:16]}"
So 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:
from praisonaiagents import Agent

agent = Agent(
    name="FileBot",
    instructions="Write files as requested",
    approval=True,
)

# First call — prompts for approval
agent.start("Write 'hello' to /tmp/a.txt")

# Second call with SAME path — no prompt (cached)
agent.start("Write 'hello again' to /tmp/a.txt")

# Third call with DIFFERENT path — prompts again
agent.start("Write 'world' to /tmp/b.txt")
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 endpoint

Troubleshooting

ErrorCauseFix
PermissionError: Approval request failed for <tool>Async agent with console backendConfigure a non-console backend
RuntimeError: Tool '<tool>' requires approval but cannot use console I/O from async context.Same root cause, surfaced earlierSame fix
Tool I expected to be called was never tried by the modelTool name is in the active deny set / preset and is pruned from the LLM’s viewEither widen permissions (e.g. approval="full") or change your permissions rules; check the agent’s _perm_deny at runtime

Best Practices

No env vars needed; you see the prompt directly in the terminal. This is now the default when running in an interactive session.
Routes approval requests to a channel humans already watch — great for CI pipelines that need human gating.
Without a timeout, the agent blocks indefinitely waiting for a decision.
approve_level: high lets safe tools run without prompts and only gates the dangerous ones.
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" 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.

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