> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Interactive Tool Approval

> Approve or deny agent tool calls interactively with persistent project rules

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](/docs/features/approval) for the full list of gated tools and bypass options, and [Approval Backends → Which class runs the prompt?](/docs/features/approval-backends) for how the Python (`ConsoleBackend`) and CLI (`InteractiveCLIApprovalBackend`) paths differ.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Coder",
    instructions="Edit files as requested",
    approval=True,
)
agent.start("Refactor utils.py")
```

The user requests a change; PraisonAI pauses on dangerous tools until they allow or deny in the terminal.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    A[Agent] --> B[Tool call]
    B --> C{Needs approval?}
    C -->|Yes| D[Terminal prompt]
    D --> E[once / session / always / no / deny & redirect]
    E -->|session| F[Reuse this run]
    E -->|always| G[Persist rule]
    E -->|deny & redirect| R[Feedback → next turn]
    C -->|No| H[Execute]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef redirect fill:#6366F1,stroke:#7C90A0,color:#fff

    class A agent
    class B tool
    class C,D check
    class E,F,G,H ok
    class R redirect

```

## Quick Start

<Steps>
  <Step title="Simple Usage">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents import Agent

    agent = Agent(
        name="Coder",
        instructions="Edit files as requested",
        approval=True,
    )
    agent.start("Refactor utils.py")
    ```
  </Step>

  <Step title="With Configuration">
    ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    praisonai --approval console run "Show me git status"
    ```

    A prompt appears:

    ```
    🔒 Tool Approval Required
    Function: execute_command
    Risk Level: MEDIUM
    Agent: Coder
    Arguments:
      command: git status -s

    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):
    ```

    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).
    * `[d] deny & redirect` — deny this call and, at the follow-up prompt (`What should the agent do instead?`), type a one-line instruction. The agent's next turn receives it as course-correction (via `ApprovalDecision.feedback`), so it doesn't retry the same call as-is.

    <Note>
      The CLI approval backend 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](#rich-%2F-textual-tui-praisonai-code-praisonai-chat).
    </Note>
  </Step>
</Steps>

## 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
* A repeated tool-call loop is detected — the interactive backend is asked (continue / stop) as a `doom_loop` decision on both sync (`chat()` / `start()`) and async (`achat()` / `astart()`) paths ([PR #4858](https://github.com/MervinPraison/PraisonAI/pull/4858) restored async parity); see [Approval → Doom-loop safety gate](/docs/features/approval#doom-loop-safety-gate)

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant U as User
    participant B as CLI approval backend
    participant R as ApprovalRegistry
    participant PM as PermissionManager
    participant A as Agent

    Note over B: Prompt shows suggested pattern:<br/>always (bash:git status *)
    B->>U: Allow?  [o] once  [s] session  [a] always  [n] no  [d] deny & redirect
    U-->>B: choice
    alt scope == "once" (approve)
        R-->>B: allow this call only
    else scope == "session"
        R->>R: _session_scoped_targets.add((agent, target))
        R-->>B: allow + reuse this run
    else scope == "always"
        R->>PM: approve(target, reusable_scope=True, pattern=…)
        PM->>PM: append to approvals.json
        R-->>B: allow + persist for future runs
    else choice == "n" (plain deny)
        R-->>A: ApprovalDecision(approved=False, feedback=None)
    else choice == "d" (deny & redirect)
        B->>U: What should the agent do instead?
        U-->>B: "edit tests/app.py instead"
        R-->>A: ApprovalDecision(approved=False, feedback="edit tests/app.py instead")
        Note over A: Next turn sees:<br/>"Tool 'execute_command' was denied by the user.<br/>Do not retry it as-is. User guidance: edit tests/app.py instead"
    end
```

## Scope choice

Five options in one row — this diagram picks the right one.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{Will you re-approve<br/>this same call soon?}
    Q -->|No, just this once| O["[o] once"]
    Q -->|Yes, several times this run| S["[s] session<br/>in-memory only"]
    Q -->|Yes, forever across runs| A["[a] always<br/>writes approvals.json"]
    Q -->|Not what I meant to run| N["[n] no"]
    Q -->|Wrong call, but I know a better one| D["[d] deny & redirect<br/>type what to do instead"]

    classDef q fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef safe fill:#10B981,stroke:#7C90A0,color:#fff
    classDef persist fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef deny fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef redirect fill:#6366F1,stroke:#7C90A0,color:#fff

    class Q q
    class O,S safe
    class A persist
    class N deny
    class D redirect
```

| Choice                | Where it lives                                                                                                        | Scope key                                                                  | Survives restart?                                             |
| --------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `[o] once`            | Nothing persisted                                                                                                     | Cache key `(tool_name, sha256(args))` (existing per-call cache)            | No                                                            |
| `[s] session`         | In-memory `ApprovalRegistry._session_scoped_targets`                                                                  | `(scope_id, permission_target)` — per Agent instance, not per display name | **No** — vanishes when the process ends                       |
| `[a] always`          | `approvals.json` via `PermissionManager`                                                                              | Suggested pattern (e.g. `bash:git status *`) with `reusable_scope=True`    | **Yes**                                                       |
| `[n] no`              | Nothing persisted                                                                                                     | —                                                                          | No                                                            |
| `[d] deny & redirect` | **Nothing persisted.** `feedback` string is fed back to the agent as the denied tool's `error_msg` for the next turn. | —                                                                          | No — feedback is one-turn only, never remembered across calls |

<Note>
  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.
</Note>

<Note>
  `session` grants are keyed by `(scope_id, permission_target)` and live in an in-memory set. `clear_approved()` wipes them, and they are never written to `approvals.json`.
</Note>

### Per-instance scoping

`[s] session` grants (and the in-context cache the fast path checks) are keyed **per Agent instance**, not per display name. Two `Agent()` objects that happen to share the same `name` — including two unnamed `Agent()` instances, which both default to the display name `"Agent"` — never inherit each other's session-scoped approvals.

<Note>
  As of [MervinPraison/PraisonAI#4533](https://github.com/MervinPraison/PraisonAI/pull/4533), the registry keys session grants by a per-instance `scope_id` (falling back to `agent_name` when unavailable, for backward compatibility). Earlier releases keyed by the shared display name, so a `[s] this session` grant on one worker silently unlocked the tool for a distinct same-named worker.
</Note>

Name-keyed lookups — per-agent backend, risk level, `ask` rules, durable `[a] always` persistence — still use `agent_name`, so YAML rules and `approvals.json` entries scoped to `agent_name: worker` continue to match every `Agent(name="worker")` across runs.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    subgraph "Two same-named workers, one session grant"
        H[👤 Human grants<br/>[s] session on Worker-1] --> W1[⚙️ Worker-1<br/>Agent name=Agent]
        H -.->|before #4533<br/>silent leak| W2[⚙️ Worker-2<br/>Agent name=Agent]
        H -->|since #4533<br/>never leaks| W1
    end

    classDef human fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef safe fill:#10B981,stroke:#7C90A0,color:#fff
    classDef leak fill:#8B0000,stroke:#7C90A0,color:#fff

    class H human
    class W1 safe
    class W2 leak
```

## How the suggested pattern is derived

The `always (…)` hint isn't magic — it maps the tool + arguments to a reusable target.

| Tool                                     | Mapping                                                              | Example target       |
| ---------------------------------------- | -------------------------------------------------------------------- | -------------------- |
| `execute_command`, `acp_execute_command` | `bash:<command>` (first non-empty of `command`/`cmd`/`code`/`query`) | `bash:git status -s` |
| `edit_file`, `acp_edit_file`             | `edit:<path>`                                                        | `edit:src/app.py`    |
| `write_file`, `acp_create_file`          | `write:<path>`                                                       | `write:src/new.py`   |
| `delete_file`, `acp_delete_file`         | `delete:<path>`                                                      | `delete:tmp/old.log` |
| `move_file`                              | `move:<src>` (uses `src`, not `dst`)                                 | `move:a/old.py`      |
| `copy_file`                              | `copy:<src>` (uses `src`, not `dst`)                                 | `copy:a/x.py`        |
| `apply_patch`                            | `tool:apply_patch` (**not** an `edit:` target)                       | `tool:apply_patch`   |
| Anything else                            | `tool:<tool_name>`                                                   | `tool:scrape_page`   |

<Warning>
  `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.
</Warning>

## Session-only workflow

Press `[s]` to auto-approve a repeated call for the rest of the run — nothing is written to disk.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Coder",
    instructions="Fix the failing tests. Use `pytest` to check as you go.",
    approval=True,   # console prompt is the default
)

agent.start("Fix the broken tests in tests/unit")
# First `bash:pytest tests/unit` call → prompt appears.
# Press [s] once → every later `bash:pytest tests/unit` in this run is auto-approved.
# Close the process → next run prompts again from scratch.
```

Pressing `[s]` on one worker never leaks to another same-named worker in the same process — session grants are keyed per Agent instance, so two `asyncio.gather(...)` agents both named `"Coder"` (or both unnamed) stay isolated.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
import asyncio
from praisonaiagents import Agent

# Two unnamed workers — both default to name="Agent"
worker_1 = Agent(instructions="Fix tests", approval=True)
worker_2 = Agent(instructions="Run linters", approval=True)

# Pressing [s] on worker_1's first pytest call NEVER auto-approves
# worker_2's pytest calls in the same process. Session grants are
# keyed per Agent instance since PR #4533.
async def run_both():
    await asyncio.gather(
        worker_1.astart("Fix broken tests in tests/unit"),
        worker_2.astart("Run ruff on src/"),
    )

asyncio.run(run_both())
```

Press `[a]` instead when the grant should persist across runs.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# Same agent, same task — press [a] instead of [s].
# The prompt suggests "always (bash:pytest *)" — press Enter to accept.
# On the next run: pytest calls no longer prompt.
# Undo with:   praisonai permissions revoke "bash:pytest *"
```

## 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 CLI approval backend keystroke prompt, the Rich frontend, and the Textual dialog.

<Note>
  **Also applies to the core-SDK console prompt.** `ConsoleBackend` — the default approval prompt on any `Agent(approval=True)`, with no `praisonai-code` required — renders the same style of unified diff for `edit_file`, `acp_edit_file`, `write_file`, `acp_create_file`, and `apply_patch`. See [Approval › Diff preview in approval prompts](/docs/features/approval#diff-preview-in-approval-prompts).
</Note>

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):

```
Preview (diff):
--- a/utils.py
+++ b/utils.py
@@ -1,3 +1,3 @@
-def add(a, b): return a+b
+def add(a: int, b: int) -> int: return a + b
```

When the caller supplies only `old_string` / `new_string` (the default `edit_file` shape):

```
Preview (diff):
--- a/utils.py
+++ b/utils.py
@@ -1 +1 @@
-def add(a, b): return a+b
+def add(a: int, b: int) -> int: return a + b
```

For `write` (up to 2000 chars, then a `\n... (truncated)` sentinel is appended), the content is prefixed with the target path:

```
# utils.py
def add(a: int, b: int) -> int:
    return a + b
```

No preview is shown for read-only or non-file tools.

<Note>
  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.
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    A[Tool call] --> B{Tool name?}
    B -->|edit / apply_patch| C{diff/patch<br/>supplied?}
    C -->|yes| D[Show supplied diff]
    C -->|no| E{old_string +<br/>new_string?}
    E -->|yes| F[Synthesise unified diff]
    E -->|no| G[No preview]
    B -->|write| H[Show content<br/>≤2000 chars, then truncate]
    B -->|other| I[No preview]

    D --> Z[Your choice]
    F --> Z
    G --> Z
    H --> Z
    I --> Z

    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef prev fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef nopre fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class A tool
    class B,C,E check
    class D,F,H prev
    class G,I nopre
    class Z ok
```

## Reject with a reason

Denying isn't just "stop" — you can steer. Two entry points collect guidance and hand it back to the agent as the denied tool's result, so the next turn course-corrects instead of retrying blindly.

* **Terminal CLI approval backend (single-key prompt)** — pick `[d] deny & redirect` at the main prompt, then answer the follow-up `What should the agent do instead?` prompt. Empty input / Ctrl-C / EOF at the follow-up falls back to a plain deny (`feedback=None`).
* **Rich / Textual TUI** — deny still opens the existing "Reason for denial (optional)" inline prompt; behaviour unchanged.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Coder",
    instructions="Edit files as requested.",
    approval=True,
)
agent.start("Refactor utils.py — feel free to run tests")
```

The agent proposes `bash: rm -rf .venv`, you deny with a redirect, and the agent picks a safer alternative on its next turn.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Console as CLI approval backend
    participant Agent

    Agent->>Console: Approve tool call?
    User->>Console: [d] deny & redirect
    Console->>User: What should the agent do instead? _
    User->>Console: "edit tests/app.py instead"
    Console->>Agent: ApprovalDecision(approved=False, feedback="edit tests/app.py instead")
    Note over Agent: Tool result becomes:<br/>"Tool 'execute_command' was denied by the user.<br/>Do not retry it as-is. User guidance: edit tests/app.py instead"
    Agent-->>User: retries with the correction

    classDef user fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef console fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff

    class User user
    class Console console
    class Agent agent
```

### How it flows

| Step                                                                       | What happens                                                             |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| 1. Agent proposes a sensitive tool call                                    | Prompt appears with the `[d] deny & redirect` key                        |
| 2. You pick **`[d]`**                                                      | The backend prompts `What should the agent do instead?`                  |
| 3. You type a one-line instruction (or press Enter to skip)                | Blank input, EOF, or Ctrl-C falls back to a plain deny (`feedback=None`) |
| 4. Backend returns `ApprovalDecision(approved=False, feedback=...)`        | `feedback` is a new `Optional[str]` field, defaulting to `None`          |
| 5. `tool_execution.py` folds `feedback` into the denied tool's `error_msg` | The agent's next turn sees the correction as the tool's result           |

### When the prompt appears

| Where you deny                                                  | Reason prompt?                                                                                      |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Console backend (single-key `[o/s/a/n/d]`)                      | **Pick `[d] deny & redirect` for the guidance follow-up. `[n]` stays a plain deny with no prompt.** |
| Rich frontend (`praisonai code`, `praisonai chat` in Rich mode) | Yes — inline prompt after Deny                                                                      |
| Textual dialog (`ApprovalDialog.on_button_pressed`)             | Accepts a `reason` argument                                                                         |
| Live Textual TUI (`y`/`n` key handler)                          | Not yet — denies without collecting a reason                                                        |
| Ctrl-C / EOF at main prompt                                     | No prompt — treated as plain denial                                                                 |
| Bot/chat-channel (Telegram / Slack / Discord)                   | Not covered by #4101 — button reject only                                                           |

<Tip>
  Skip the guidance (press Enter at the follow-up, or pick plain `[n]`) when the agent's proposal is obviously wrong and no correction is worth typing — the plain-denial path is unchanged.
</Tip>

<Note>
  The guidance 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. Blank input keeps today's plain denial.
</Note>

## 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. The Rich/Textual TUI ships with `praisonai-code`, so this dialog is available standalone (PR #3818) — no wrapper required.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai code "Refactor utils.py"
```

When the agent runs `git status`, the dialog shows the exact pattern each option will persist:

```
Approval Required — shell_command: git status -s

  ▸ Once
  ▸ Session Only          (shell_command:git status *)
  ▸ Always Allow          (shell_command:git status *)
  ▸ Always Allow All      (tool:shell_command)
  ▸ Deny
```

Both Rich and Textual now render the same change preview the CLI approval backend 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.

<Note>
  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.
</Note>

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    P[Approval prompt<br/>shell_command: git status -s]
    P --> AA["Always Allow"]
    P --> SO["Session Only"]
    P --> AAA["Always Allow All"]
    P --> D["Deny"]

    AA --> N["Narrow: shell_command:git status *<br/>persisted to approvals.json"]
    SO --> M["Narrow: shell_command:git status *<br/>in-memory this run only"]
    AAA --> B["Blanket: tool:shell_command<br/>persisted to approvals.json"]
    D --> X["No rule saved"]

    classDef prompt fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef narrow fill:#10B981,stroke:#7C90A0,color:#fff
    classDef blanket fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef deny fill:#8B0000,stroke:#7C90A0,color:#fff

    class P prompt
    class AA,SO,AAA,D prompt
    class N,M narrow
    class B blanket
    class X deny
```

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.

<Warning>
  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.
</Warning>

<Note>
  Building a custom TUI on top of `InteractiveCore`? Use `derive_permission_pattern(request, scope=...)` in [`praisonai_code/cli/interactive/events.py`](https://github.com/MervinPraison/PraisonAI/blob/main/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>`.
</Note>

## Approval modes

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{What are you doing?}
    Q -->|Exploring read-only| P[plan]
    Q -->|Editing files| E[accept-edits]
    Q -->|Full control| C[console / default]
    Q -->|Trusted sandbox only| B[bypass]

    classDef safe fill:#10B981,stroke:#7C90A0,color:#fff
    classDef mod fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef danger fill:#EF4444,stroke:#7C90A0,color:#fff

    class P safe
    class E,C mod
    class B danger
```

| CLI flag                  | `PermissionMode` | Value                | Behaviour                              |
| ------------------------- | ---------------- | -------------------- | -------------------------------------- |
| `--approval console`      | `DEFAULT`        | `default`            | Prompt for each sensitive call         |
| `--approval plan`         | `PLAN`           | `plan`               | Block write, edit, delete, bash, shell |
| `--approval accept-edits` | `ACCEPT_EDITS`   | `accept_edits`       | Auto-approve edit/write tools          |
| `--approval bypass`       | `BYPASS`         | `bypass_permissions` | Skip all checks                        |

<Warning>
  The CLI uses `--approval bypass` but the enum value is `bypass_permissions`.
</Warning>

## 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:

| Choice                | Scope                                               | Persisted pattern example                                                                                         |
| --------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `[o] once`            | This call only                                      | *(not persisted)*                                                                                                 |
| `[s] this session`    | In-memory only — nothing written                    | *(not persisted)*                                                                                                 |
| `[a] always`          | Narrow command-prefix / path                        | `bash:git status *`, `edit:utils.py`, `write:.env`                                                                |
| `[n] no`              | This call only                                      | *(not persisted)*                                                                                                 |
| `[d] deny & redirect` | This call only, with one-turn feedback to the agent | *(not persisted — feedback is passed to the model as the denied tool's result and forgotten after the next turn)* |

`[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()`.

<Note>
  `[a] always` grants are still keyed by `agent_name` in `approvals.json` — they must match across process restarts, and only the display name survives a restart. `[s] session` and the in-context cache are keyed per Agent instance (`scope_id`) so unnamed workers in one process never inherit each other's runtime approvals.
</Note>

<Tip>
  Tune the derived pattern with [Reusable Approval Scopes](/docs/features/reusable-approval-scopes) — call `PermissionManager.suggest_scope_pattern(target)` for a custom UI, or hand-author `bash:git *` via `praisonai permissions allow`.
</Tip>

Manage rules with:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai permissions list
praisonai permissions allow "bash:git *"
```

| File             | Shared?                     |
| ---------------- | --------------------------- |
| `rules.json`     | Yes — commit for team rules |
| `approvals.json` | No — local session data     |

<Accordion title="approvals.json entry format">
  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](/docs/docs/features/permissions#reusable-command-prefix-approvals) — user-edited or hand-authored patterns save with `derived: false`. Old files without the field load cleanly (`derived` defaults to `False`).
</Accordion>

<Tip>
  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](/docs/features/reusable-approval-scopes).
</Tip>

## Non-interactive and CI

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai --yes --approval console run "Check deployment"
PRAISONAI_NON_INTERACTIVE=1 praisonai --approval console run "Check deployment"
```

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

## Best Practices

<AccordionGroup>
  <Accordion title="Start with plan for new repos">
    Use `--approval plan` until you trust the agent's behaviour in a codebase.
  </Accordion>

  <Accordion title="Review external tools">
    Tools marked `external` always prompt — verify third-party integrations before allowing.
  </Accordion>

  <Accordion title="Share rules.json in git">
    Team-wide allow/deny patterns belong in version control.
  </Accordion>

  <Accordion title="`Always allow` is now narrow by default — pick `Always Allow All` explicitly for a blanket rule">
    In both the CLI approval backend 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:*"`.

    A `[a]` always grant picked at the ConsoleBackend prompt is now recorded into the registry (`_persist_scoped_decision`) and honoured by the fast path on every subsequent matching call in the same run. Before [PR #4878](https://github.com/MervinPraison/PraisonAI/pull/4878) the backend path never recorded it, so the reusable pattern was re-prompted on the very next call. See [Standing grants and attached backends](/docs/features/approval#standing-grants-and-attached-backends).
  </Accordion>

  <Accordion title="Prefer [s] session over [a] always for one-off runs">
    `[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.

    A `[s]` session grant picked at the ConsoleBackend prompt is now persisted into the registry (`_persist_scoped_decision`) and honoured by the fast path on every subsequent matching call in the same run. Before [PR #4878](https://github.com/MervinPraison/PraisonAI/pull/4878) this grant was forgotten immediately and the user was re-prompted on the very next call. See [Standing grants and attached backends](/docs/features/approval#standing-grants-and-attached-backends).
  </Accordion>

  <Accordion title="Skim the change preview before pressing [a]">
    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.
  </Accordion>

  <Accordion title="Give a name to your agent before pressing [a]">
    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.
  </Accordion>

  <Accordion title="Reach for [d] when the agent is close but wrong">
    Use `[d] deny & redirect` when the agent's proposal is *nearly* right but you can point it at the correct target in a sentence — "edit `tests/app.py` instead", "use `staging.env`, not `production.env`", "run `pytest -k login` first". The next turn sees your instruction as the denied tool's result and adjusts, instead of retrying the same call. Use plain `[n]` when the agent just needs to stop.
  </Accordion>
</AccordionGroup>

## 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.

<Card title="Interactive Callback Authorization" icon="user-shield" href="/docs/features/interactive-callback-authorization">
  Lock approval buttons to specific users in shared chats — covers Telegram, Slack, and Discord bots.
</Card>

<Card icon="function" href="/docs/sdk/reference/praisonaiagents/modules/permissions">
  `derive_pattern` — shared narrow-pattern derivation used by CLI, YAML, and Python rules
</Card>

## Related

<CardGroup cols={2}>
  <Card title="Permissions CLI" icon="terminal" href="/docs/cli/permissions">
    `praisonai permissions` reference
  </Card>

  <Card title="Permission Modes" icon="shield" href="/docs/features/permission-modes">
    All modes for agents and CLI
  </Card>

  <Card title="Permissions Module" icon="shield-halved" href="/docs/features/permissions">
    Python SDK API
  </Card>
</CardGroup>
