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

# Permissions Module

> Pattern-based permission rules, persistent approvals, and doom loop detection

Pattern-based rules let you allow, deny, or ask about tool calls — with persistent approvals and doom loop detection.

<Info>
  For the CLI, see [`praisonai permissions`](/docs/cli/permissions) and [Interactive Tool Approval](/docs/features/interactive-approval).
</Info>

<Note>
  To also block reads, writes, and shell commands **outside** a project root, see [Workspace Boundary](/docs/features/workspace-boundary).
</Note>

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

agent = Agent(
    name="Safe Coder",
    instructions="Run shell commands safely",
    approval={
        "permissions": {
            "read:*": "allow",
            "bash:rm *": "deny",
            "*": "ask",
        },
    },
)
agent.start("List files in the project root")
```

The user prompts the agent; permission rules allow, deny, or ask before each tool executes.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Agent[🤖 Agent] --> Check{Permission Check}
    Check -->|allow| Run[✅ Execute]
    Check -->|deny| Block[❌ Block]
    Check -->|ask| Prompt[❓ User prompt]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef block fill:#EF4444,stroke:#7C90A0,color:#fff

    class Agent agent
    class Check check
    class Run ok
    class Block block
    class Prompt check
```

## How It Works

Two seams enforce a deny rule: the schema-hide seam at build time and the call-time gate — both for native and MCP tools.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Model as LLM
    participant Agent
    participant Mgr as PermissionManager
    participant Tool as Tool (native or MCP)

    Note over Agent,Mgr: Schema build
    Agent->>Mgr: is_denied(tool_name)?
    Mgr-->>Agent: deny -> drop from schema

    Note over Model,Tool: Call time (native OR MCP)
    Model->>Agent: call(tool_name, args)
    Agent->>Mgr: is_denied(tool_name)?
    Mgr-->>Agent: deny -> block
    Agent->>Tool: execute (when allowed)
```

## Quick Start

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

    agent = Agent(
        name="CI Worker",
        instructions="Run safely without prompts",
        approval={
            "permissions": {
                "read:*": "allow",
                "bash:rm *": "deny",
            },
        },
    )
    agent.start("Check the deployment status")
    ```
  </Step>

  <Step title="With Configuration">
    Use `PermissionManager` directly for programmatic rule management:

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonaiagents.permissions import (
        PermissionManager,
        PermissionRule,
        PermissionAction,
    )

    manager = PermissionManager()
    manager.add_rule(PermissionRule(
        pattern="bash:rm *",
        action=PermissionAction.DENY,
        description="Block destructive commands",
    ))
    manager.add_rule(PermissionRule(
        pattern="read:*",
        action=PermissionAction.ALLOW,
    ))

    result = manager.check("bash:rm -rf /tmp")
    print(result.is_denied)  # True
    ```

    For YAML and CLI surfaces, see [Declarative Permissions](/docs/features/declarative-permissions).
  </Step>
</Steps>

***

## Auto-wiring from approval config

Passing `approval={"permissions": {...}}` to `Agent(...)` now attaches a `PermissionManager` automatically, so pattern-based rules both hide denied tools from the model and block them at call time.

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

agent = Agent(
    name="CI Worker",
    instructions="Ship safely",
    approval={
        "permissions": {
            "read:*": "allow",
            "bash:rm *": "deny",   # hidden from the model + blocked at call time
            "*": "ask",
        },
    },
)
agent.start("Check the deployment status")
```

The agent accepts three shapes for `permissions`:

| Shape                                | Behaviour                                                                        |
| ------------------------------------ | -------------------------------------------------------------------------------- |
| `dict` (flat `"pattern": "action"`)  | Loaded via `PermissionManager(agent_name=...).load_rules_from_config(dict)`      |
| `list` / `tuple` of `PermissionRule` | Each rule added via `add_rule`                                                   |
| `PermissionManager` instance         | **Reused as-is** — no throwaway manager, no `~/.praisonai/permissions/` disk I/O |

Pass a pre-built manager to reuse it directly:

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

mgr = PermissionManager(storage_dir="/tmp/policy")
mgr.load_rules_from_config({"bash:rm *": "deny", "read:*": "allow"})

agent = Agent(
    name="Deployer",
    instructions="Deploy safely",
    approval={"permissions": mgr},   # reused as-is
)
```

Wiring failures are non-fatal — logged at DEBUG. The preset frozenset tier and the call-time gate stay in effect.

***

## MCP tools are gated uniformly

The same deny gate applies to MCP tools: both native functions and MCP tools flow through `_execute_tool_impl`, so `deny` hides and blocks them identically. MCP tools using a `tool:<name>` prefix are matched by rules against either the bare name or the prefixed form.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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",   # matches MCP-namespaced tool names
        },
    },
)
# delete_* MCP tools never appear in the schema and are blocked at call time
# with {"permission_denied": True}.
```

***

## Permission Actions

| Action  | Description                       |
| ------- | --------------------------------- |
| `ALLOW` | Automatically allow the operation |
| `DENY`  | Automatically deny the operation  |
| `ASK`   | Require user approval             |

<Info>
  Since `praisonai-agents` v1.6.91, tools matched as `deny` are also **removed from the LLM-advertised set** (function schema and system prompt) — not just blocked at execution. See [Approval › How tools are pruned from the LLM](/docs/features/approval#how-tools-are-pruned-from-the-llm).
</Info>

***

## Permission Modes

Global modes for subagent delegation — see [Permission Modes](/docs/features/permission-modes) for full details.

| Mode           | Value                | Description                  |
| -------------- | -------------------- | ---------------------------- |
| `DEFAULT`      | `default`            | Standard permission checking |
| `PLAN`         | `plan`               | Read-only exploration        |
| `ACCEPT_EDITS` | `accept_edits`       | Auto-accept file edits       |
| `DONT_ASK`     | `dont_ask`           | Auto-deny all prompts        |
| `BYPASS`       | `bypass_permissions` | Skip all checks              |

***

## Configuration Options

### PermissionRule

| Option        | Type               | Default | Description                         |
| ------------- | ------------------ | ------- | ----------------------------------- |
| `pattern`     | `str`              | —       | Glob or regex pattern to match      |
| `action`      | `PermissionAction` | `ASK`   | `allow`, `deny`, or `ask`           |
| `description` | `str`              | `""`    | Human-readable description          |
| `is_regex`    | `bool`             | `False` | Use regex instead of glob           |
| `priority`    | `int`              | `0`     | Higher priority rules checked first |
| `agent_name`  | `str`              | `None`  | Apply to a specific agent only      |
| `enabled`     | `bool`             | `True`  | Whether the rule is active          |

### PermissionManager

| Option              | Type       | Default                    | Description                                                                                                                                                                                                                                                                                        |
| ------------------- | ---------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `storage_dir`       | `str`      | `~/.praisonai/permissions` | Directory for persistent approvals                                                                                                                                                                                                                                                                 |
| `agent_name`        | `str`      | `None`                     | Filter rules to one agent                                                                                                                                                                                                                                                                          |
| `approval_callback` | `Callable` | `None`                     | Custom callback for user approval                                                                                                                                                                                                                                                                  |
| `workspace_root`    | `str`      | `None`                     | Optional project/workspace root. When set, shell and file targets that resolve outside this root emit a distinct `external_dir:<parent>/*` sub-target defaulting to `ask`. When `None`, no boundary check runs (backward compatible). See [Workspace Boundary](/docs/features/workspace-boundary). |

### `approve()` — reusable scope kwargs

Available on `PermissionManager.approve(target, approved, scope, agent_name, reusable_scope, pattern)`:

| Option           | Type          | Default | Description                                                                                                                                           |
| ---------------- | ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reusable_scope` | `bool`        | `False` | When `True` and `scope` is `"session"` or `"always"`, store the derived prefix glob instead of the literal command. Ignored when `pattern=` is given. |
| `pattern`        | `str \| None` | `None`  | Explicit pattern to record. Overrides both `target` and `reusable_scope` — lets a CLI/UI show the suggestion and let the user tweak it before saving. |

### PersistentApproval

| Option     | Type   | Default  | Description                                                                                                                                                                                                                                                   |
| ---------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pattern`  | `str`  | —        | Glob pattern this approval covers (fnmatch semantics)                                                                                                                                                                                                         |
| `approved` | `bool` | —        | Whether the action was approved                                                                                                                                                                                                                               |
| `scope`    | `str`  | `"once"` | `once`, `session`, or `always`                                                                                                                                                                                                                                |
| `derived`  | `bool` | `False`  | `True` when `pattern` was auto-generated by [reusable-scope derivation](#reusable-command-prefix-approvals). Gates a small bare-prefix match extension so `bash:git status *` also matches the bare `bash:git status`. User-authored globs stay pure fnmatch. |

***

### PermissionManager.approve()

Record an approval decision. Returns a `PersistentApproval`.

| Arg              | Type            | Default  | Description                                                                                                                                                                           |
| ---------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `target`         | `str`           | —        | The exact action being approved, e.g. `"bash:git status -s"`                                                                                                                          |
| `approved`       | `bool`          | —        | `True` to allow, `False` to deny                                                                                                                                                      |
| `scope`          | `str`           | `"once"` | `once` (this call only), `session` (this process), `always` (persisted)                                                                                                               |
| `agent_name`     | `str` \| `None` | `None`   | Restrict this approval to one agent                                                                                                                                                   |
| `reusable_scope` | `bool`          | `False`  | Opt in to store a generalised prefix glob instead of the literal `target` (see [Reusable command-prefix approvals](#reusable-command-prefix-approvals)). Ignored when `scope="once"`. |
| `pattern`        | `str` \| `None` | `None`   | Explicit pattern to store. Overrides both `target` and `reusable_scope` — useful when CLI/UI displays a suggested scope for the user to edit before saving.                           |

***

### PermissionManager.is\_denied()

Cheap, callback-free helper used at schema-build time to hide tools the model can't call.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
def is_denied(self, tool_name: str, agent_name: Optional[str] = None) -> bool:
    """Return True if tool_name resolves to a hard deny.

    Only DENY hides the tool; ask/allow/no-match keep it visible so approval
    can still happen at execution time (defence in depth).

    Checks both bare and tool:<name> forms. Never consults the interactive
    approval callback — safe to call while assembling the schema.
    """
```

Returns `True` **only** when a matching rule resolves to a hard `DENY`. `allow`, `ask`, and the no-matching-rule default of `ask` all return `False` — those tools stay visible so approval can still run at call time. It checks both the bare name and the `tool:<name>` form, so a rule against either convention takes effect (important for MCP-namespaced names). Deny-wins is global: the first target form that resolves to `DENY` returns `True`.

| Arg          | Type            | Default | Description                                                                    |
| ------------ | --------------- | ------- | ------------------------------------------------------------------------------ |
| `tool_name`  | `str`           | —       | Tool name to check — bare (`"write_file"`) or namespaced (`"tool:write_file"`) |
| `agent_name` | `str` \| `None` | `None`  | Restrict the check to one agent's rules                                        |

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

mgr = PermissionManager()
mgr.load_rules_from_config({"write_file": "deny", "read_file": "allow"})

mgr.is_denied("write_file")       # True   -> hidden from the schema
mgr.is_denied("read_file")        # False  -> allow, stays advertised
mgr.is_denied("edit_file")        # False  -> no matching rule = ask, not deny
mgr.is_denied("tool:write_file")  # True   -> namespaced form
```

<Note>
  `PermissionManager.is_denied(...)` is a **manager method** — an exposure-time helper. `PermissionResult.is_denied` (documented above at the `check()` result) is a **result attribute** describing a single `check()` outcome. The method never calls the interactive approval callback; the attribute reflects an already-resolved decision.
</Note>

***

## Reusable command-prefix approvals

Approving `git status` once should not force a new prompt for `git status -s`. Opt in with `reusable_scope=True` on a `session`/`always` approval and PraisonAI derives a generalised glob from a small command-arity table:

| Command approved            | Stored pattern          | Also allows                                            |
| --------------------------- | ----------------------- | ------------------------------------------------------ |
| `bash:git status -s`        | `bash:git status *`     | `bash:git status`, `bash:git status .`                 |
| `bash:npm run build`        | `bash:npm run *`        | `bash:npm run test`, `bash:npm run lint`               |
| `bash:docker compose up -d` | `bash:docker compose *` | `bash:docker compose down`, `bash:docker compose logs` |
| `bash:ls -la`               | `bash:ls *`             | `bash:ls`, `bash:ls src/`                              |

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

manager = PermissionManager()

# Preview what would be stored (safe to call — never records anything)
suggested = manager.suggest_scope_pattern("bash:git status -s")
# → "bash:git status *"

# Record the approval with the reusable prefix
manager.approve(
    "bash:git status -s",
    approved=True,
    scope="always",
    reusable_scope=True,
)

manager.check("bash:git status").is_allowed   # True
manager.check("bash:git status .").is_allowed  # True
manager.check("bash:git commit").needs_approval  # True — different subcommand still asks
```

### What is NOT generalised (conservative by design)

| Input                            | Result                        | Why                                                                                         |
| -------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
| `bash:git` (bare, no subcommand) | `bash:git` (literal)          | A globbed prefix would auto-approve every git subcommand                                    |
| `bash:cd /tmp && rm x`           | literal                       | A `&&`/`\|`/`;`/`$()`/`>` operator would swallow the second command into the reusable scope |
| `bash:rm *` (already a glob)     | `bash:rm *` (unchanged)       | The user has already declared the pattern they want                                         |
| `read:/etc/hosts`                | `read:/etc/hosts` (unchanged) | Only `bash:`/`shell:` targets are generalised                                               |
| Unknown command (`cowsay hello`) | `bash:cowsay *`               | Matches only trailing args, never the whole command space                                   |

### The `derived` flag

A reusable-scope approval is stored with `derived=True` (persisted in `approvals.json`). That flag gates a small extra rule in `PersistentApproval.matches`: for a derived pattern that ends in `" *"` and starts with `bash:`/`shell:`, the bare prefix also matches. So `bash:git status *` (derived) matches both `bash:git status -s` **and** the plain `bash:git status`.

Your own hand-authored `bash:rm *` keeps exact fnmatch semantics: `bash:rm` does **not** match it, matching the behaviour you had before this feature.

### Custom arity table

The default table covers common tools (`git`, `gh`, `npm`, `pnpm`, `pip`, `cargo`, `go`, `docker`, `docker compose`, `kubectl`, `helm`, `python`, `pytest`, `ruff`, `apt`, `brew`, `systemctl`, and more). Longest multi-word key wins (so `docker compose` beats `docker`). To override for one call, use the low-level helpers:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.permissions import derive_pattern, command_prefix

command_prefix(["mytool", "run", "job"], {"mytool": 2})
# → "mytool run"

derive_pattern("bash:mytool run job", {"mytool": 2})
# → "bash:mytool run *"
```

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Agent
    participant Feature as Permissions Module

    User->>Agent: Request
    Agent->>Feature: Process request
    Feature-->>Agent: Result
    Agent-->>User: Response
```

Compound shell commands (`&&`, `||`, `|`, subshells) are decomposed and evaluated independently — deny beats ask beats allow. See [Command-Aware Permissions](/docs/features/command-aware-permissions).

When `workspace_root` is set, any path that escapes the root emits an `external_dir:<parent>/*` sub-target — a first-class permission target alongside `bash:`, `write:`, and `read:`. Set it once to stop broad approvals from granting whole-machine access. See [Workspace Boundary](/docs/features/workspace-boundary).

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
manager = PermissionManager()
manager.add_rule(PermissionRule(pattern="bash:rm *", action=PermissionAction.DENY))

manager.check("bash:cd /tmp && rm -rf x").is_denied  # True — rm in compound command
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Deny destructive commands first">
    Add high-priority deny rules for `bash:rm *`, `bash:sudo *`, and similar patterns before broader allow rules.
  </Accordion>

  <Accordion title="Use session scope for repeated tools">
    Approve with `scope="session"` so a tool call doesn't re-prompt every time in one run. For shell commands whose flags/args change (`git status -s` vs `git status .`), add `reusable_scope=True` so the approval covers the whole subcommand instead of just the literal string. See [Reusable Approval Scopes](/docs/features/reusable-approval-scopes).
  </Accordion>

  <Accordion title="Set per-agent rules for teams">
    Use `agent_name` on rules when a coder agent may write but a reviewer agent must stay read-only.
  </Accordion>

  <Accordion title="Watch for doom loops">
    `DoomLoopDetector` flags repeated identical tool calls — reset or change strategy when `is_loop` is true.
  </Accordion>
</AccordionGroup>

***

## CLI Reference

The `praisonai permissions` subcommands let you manage permission rules from the terminal.

<Note>
  These subcommands were unreachable in earlier versions due to a lazy-loader bug. They are fully functional in the current release.
</Note>

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# List all active permission rules
praisonai permissions list

# Allow a pattern (e.g. allow all read operations)
praisonai permissions allow "read:*"

# Deny a pattern (e.g. deny all rm commands)
praisonai permissions deny "bash:rm *"

# Prompt user for approval matching a pattern
praisonai permissions ask "bash:*"

# Remove a rule
praisonai permissions remove "bash:rm *"

# Reset all rules to defaults
praisonai permissions reset

# Export rules to a file
praisonai permissions export rules.json

# Import rules from a file
praisonai permissions import-rules rules.json
```

| Subcommand            | Description                                |
| --------------------- | ------------------------------------------ |
| `list`                | List all current permission rules          |
| `allow <pattern>`     | Add an allow rule for a tool call pattern  |
| `deny <pattern>`      | Add a deny rule for a tool call pattern    |
| `ask <pattern>`       | Add a rule to prompt the user for approval |
| `remove <pattern>`    | Remove a rule matching the pattern         |
| `reset`               | Reset all rules to factory defaults        |
| `export <file>`       | Export all rules to a JSON file            |
| `import-rules <file>` | Import rules from a JSON file              |

***

## Related

<CardGroup cols={2}>
  <Card title="Declarative Permissions" icon="file-code" href="/docs/features/declarative-permissions">
    YAML, CLI, and Python permission policies
  </Card>

  <Card title="Command-Aware Permissions" icon="shield-halved" href="/docs/features/command-aware-permissions">
    How compound shell commands are evaluated
  </Card>

  <Card title="Workspace Boundary" icon="shield-halved" href="/docs/features/workspace-boundary">
    Gate shell and file access outside your project root
  </Card>

  <Card title="Reusable Approval Scopes" icon="list-check" href="/docs/features/reusable-approval-scopes">
    Approve once, cover arg variants with prefix globs
  </Card>
</CardGroup>
