> ## 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/docs/features/approval) for the full list of gated tools and bypass options.

```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]
    E -->|session| F[Reuse this run]
    E -->|always| G[Persist rule]
    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

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

```

## 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
    Choice [o/s/a/n] (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).

    <Note>
      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](#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

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant U as User
    participant B as ConsoleBackend
    participant R as ApprovalRegistry
    participant PM as PermissionManager

    Note over B: Prompt shows suggested pattern:<br/>always (bash:git status *)
    B->>U: Allow?  [o] once  [s] session  [a] always  [n] no
    U-->>B: choice
    B->>R: ApprovalDecision(scope, scope_pattern)

    alt scope == "once"
        R-->>B: allow this call only
    else scope == "session"
        R->>R: _session_scoped_targets.add((agent, target))<br/>(in-memory only)
        R-->>B: allow + reuse for matching calls 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
    end
```

## Scope choice

Four 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"]

    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

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

| 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` | `(agent_name, permission_target)`                                       | **No** — vanishes when the process ends |
| `[a] always`  | `approvals.json` via `PermissionManager`             | Suggested pattern (e.g. `bash:git status *`) with `reusable_scope=True` | **Yes**                                 |

<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 `(agent_name, permission_target)` and live in an in-memory set. `clear_approved()` wipes them, and they are never written to `approvals.json`.
</Note>

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

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

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

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.

```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 reason, and the agent picks a safer alternative on its next turn.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant TUI as Rich TUI
    participant Agent

    Agent->>TUI: Approve tool call?
    User->>TUI: Deny
    TUI->>User: Reason for denial (optional): _
    User->>TUI: "don't touch .venv"
    TUI->>Agent: ApprovalResponse(REJECT, reason="don't touch .venv")
    Agent->>Agent: sees denied + reason as tool result
    Agent-->>User: retries with a different plan

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

    class User user
    class TUI tui
    class Agent agent
```

### How it flows

| Step                                                              | What happens                                                          |
| ----------------------------------------------------------------- | --------------------------------------------------------------------- |
| 1. Agent proposes a sensitive tool call                           | Rich option list appears                                              |
| 2. You pick **Deny**                                              | The frontend prints `Reason for denial (optional, steers the agent):` |
| 3. You type a reason (or press Enter to skip)                     | Blank input, EOF, or Ctrl-C preserves today's plain-denial behaviour  |
| 4. Frontend emits `ApprovalResponse(decision=REJECT, reason=...)` | Reason is included in `to_dict()` for observability                   |
| 5. Core returns the reason as the denied tool's result            | The agent's next turn sees *why* and can steer                        |

### When the prompt appears

| Where you deny                                                  | Reason 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, kept ready for a reason-capture widget |
| Live Textual TUI (`y`/`n` key handler)                          | Not yet — denies without collecting a reason                        |
| Console backend (single-key `[o/s/a/n]`)                        | Already prompted for a reason before #3180                          |
| Ctrl-C / EOF                                                    | No prompt — treated as plain denial (unchanged)                     |
| Bot/chat-channel (Telegram / Slack / Discord)                   | Not covered by #3180 — button reject only                           |

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

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

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

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

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

<Tip>
  Tune the derived pattern with [Reusable Approval Scopes](/docs/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/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 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:*"`.
  </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.
  </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>
</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/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/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/docs/cli/permissions">
    `praisonai permissions` reference
  </Card>

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

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