Skip to main content
Pattern-based rules let you allow, deny, or ask about tool calls — with persistent approvals and doom loop detection.
To also block reads, writes, and shell commands outside a project root, see Workspace Boundary.
Reading secret files (.env, private keys, *.pem, id_rsa, etc.) now defaults to ask — even with a broad "read:*": "allow" rule. A catch-all read allow no longer silently authorises credential files; you need a secret-specific rule (e.g. "read:*.env": "allow") to opt in. See Built-in Secret-File Read Gate.
Shell commands containing $IFS, ${VAR}, or bare $VAR are escalated to ASK rather than silently allowed — see Command-Aware Permissions → Shell expansion escalates to ASK.
The user prompts the agent; permission rules allow, deny, or ask before each tool executes.

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.

Quick Start

1

Simple Usage

2

With Configuration

Use PermissionManager directly for programmatic rule management:
For YAML and CLI surfaces, see Declarative Permissions.
Permission rules resolve identically whether you run a direct prompt or a YAML workflow — praisonai run workflow.yaml --deny "bash:rm *" uses the same CLI-flag + project-config merge as praisonai run "<prompt>". A permission rule with no --approval implicitly selects the console backend so the rule is enforced. See Approval › YAML workflow approval gating.

Built-in Secret-File Read Gate

Reading a .env or private-key file and forwarding its contents to the model provider is a silent secret-leak path. The core permission manager closes it by defaulting reads of well-known credential-file names to ask — even when a broad "read:*": "allow" rule is in effect. Safe example/template files stay allowed, and explicit user rules always override.

Gated basenames

Case-insensitive fnmatch against the file’s basename (path prefix ignored — config/.env and .env both gate).

Not gated (safe siblings)

Documentation templates without real secrets fall through to normal rules — a broad read:* allow reads them without prompting:

Override precedence

What counts as “secret-specific”?

A rule is a secret-specific opt-in when its glob does not match ordinary files. A glob that also matches ordinary files (like read:*) is treated as broad and does not silently authorise secrets:

Quick-start examples

1

Default — reading .env prompts

Reading .env prompts for approval — no configuration required.
2

Opt in — secret-specific allow

Add a secret-specific allow rule — a broad read:* alone won’t do.
3

Harden — secret-specific deny

Add a secret-specific deny rule to reject even with user approval.
Non-read targets are unaffected. write:.env, delete_file:.env, and any non-read-prefixed target follow the ordinary rule flow — this gate only intercepts read: and read_file: prefixes.Non-secret reads are unaffected. A broad "read:*": "allow" still allows ordinary files like main.py, README.md, data.txt — only credential-file basenames trigger the gate.No filesystem access. The gate is purely pattern-based on the target string. It does not stat the file or read its contents.

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.
The agent accepts three shapes for permissions: Pass a pre-built manager to reuse it directly:
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.

Permission Actions

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.

Permission Modes

Global modes for subagent delegation — see Permission Modes for full details.

Configuration Options

PermissionRule

Agent-scoped approvals require a named caller. A rule/approval with agent_name="support" matches only callers whose agent_name is exactly "support". Callers that don’t provide an agent_name (None) are rejected — an unnamed caller cannot inherit another agent’s grant. Unscoped rules (agent_name=None) are unaffected and still match every caller. This guarantee was tightened in PraisonAI PR #3545.

PermissionManager

approve() — reusable scope kwargs

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

PersistentApproval


PermissionManager.approve()

Record an approval decision. Returns a PersistentApproval.

PermissionManager.is_denied()

Cheap, callback-free helper used at schema-build time to hide tools the model can’t call.
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.
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.

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:

What is NOT generalised (conservative by design)

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:

Approval scope lifecycle

PersistentApproval.scope controls how long an approval lives.
once is the interactive-prompt default — pressing [o] at the CLI records an approval that expires the moment the tool call it authorised finishes. Reach for session or always if you want the approval to cover follow-up calls.

How It Works

Compound shell commands (&&, ||, |, subshells) are decomposed and evaluated independently — deny beats ask beats allow. See 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.

Best Practices

Add high-priority deny rules for bash:rm *, bash:sudo *, and similar patterns before broader allow rules.
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.
Use agent_name on rules when a coder agent may write but a reviewer agent must stay read-only.
When you grant an approval scoped to a specific agent (agent_name="support"), make sure the caller side also sets agent_name="support". Unnamed callers (agent_name=None) do not match a scoped approval — this is intentional (see the note above).
DoomLoopDetector flags repeated identical tool calls — reset or change strategy when is_loop is true.
The gate exists because a catch-all read:* allow is the exact pattern that lets an agent forward credentials to the model provider. If your CI genuinely needs to read .env, add "read:*.env": "allow" alongside "read:*": "allow" so the intent is auditable.
Template files with placeholder values are safe by design — they match the allowlist and stay readable. Don’t rename them to .env “for consistency”; renaming triggers the gate.
An interactive approval (always or session scope) on a specific read:.env target survives the gate. Use praisonai permissions approve to bake this into your project once instead of a rule.
Rules created with is_regex=True are always treated as secret-specific opt-ins on the assumption the author was explicit. Prefer named globs over regex when writing rules for secrets so the intent is obvious to reviewers.

CLI Reference

The praisonai permissions subcommands let you manage permission rules from the terminal.
These subcommands were unreachable in earlier versions due to a lazy-loader bug. They are fully functional in the current release.

Declarative Permissions

YAML, CLI, and Python permission policies

Command-Aware Permissions

How compound shell commands are evaluated

Workspace Boundary

Gate shell and file access outside your project root

Reusable Approval Scopes

Approve once, cover arg variants with prefix globs