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

How It Works

Quick Start

1

Simple Usage

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")
2

With Configuration

Use PermissionManager directly for programmatic rule management:
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.

Permission Actions

ActionDescription
ALLOWAutomatically allow the operation
DENYAutomatically deny the operation
ASKRequire user approval
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.
ModeValueDescription
DEFAULTdefaultStandard permission checking
PLANplanRead-only exploration
ACCEPT_EDITSaccept_editsAuto-accept file edits
DONT_ASKdont_askAuto-deny all prompts
BYPASSbypass_permissionsSkip all checks

Configuration Options

PermissionRule

OptionTypeDefaultDescription
patternstrGlob or regex pattern to match
actionPermissionActionASKallow, deny, or ask
descriptionstr""Human-readable description
is_regexboolFalseUse regex instead of glob
priorityint0Higher priority rules checked first
agent_namestrNoneApply to a specific agent only
enabledboolTrueWhether the rule is active

PermissionManager

OptionTypeDefaultDescription
storage_dirstr~/.praisonai/permissionsDirectory for persistent approvals
agent_namestrNoneFilter rules to one agent
approval_callbackCallableNoneCustom callback for user approval
workspace_rootstrNoneOptional 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.

approve() — reusable scope kwargs

Available on PermissionManager.approve(target, approved, scope, agent_name, reusable_scope, pattern):
OptionTypeDefaultDescription
reusable_scopeboolFalseWhen True and scope is "session" or "always", store the derived prefix glob instead of the literal command. Ignored when pattern= is given.
patternstr | NoneNoneExplicit 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

OptionTypeDefaultDescription
patternstrGlob pattern this approval covers (fnmatch semantics)
approvedboolWhether the action was approved
scopestr"once"once, session, or always
derivedboolFalseTrue when pattern was auto-generated by reusable-scope derivation. 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.
ArgTypeDefaultDescription
targetstrThe exact action being approved, e.g. "bash:git status -s"
approvedboolTrue to allow, False to deny
scopestr"once"once (this call only), session (this process), always (persisted)
agent_namestr | NoneNoneRestrict this approval to one agent
reusable_scopeboolFalseOpt in to store a generalised prefix glob instead of the literal target (see Reusable command-prefix approvals). Ignored when scope="once".
patternstr | NoneNoneExplicit pattern to store. Overrides both target and reusable_scope — useful when CLI/UI displays a suggested scope for the user to edit before saving.

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 approvedStored patternAlso allows
bash:git status -sbash:git status *bash:git status, bash:git status .
bash:npm run buildbash:npm run *bash:npm run test, bash:npm run lint
bash:docker compose up -dbash:docker compose *bash:docker compose down, bash:docker compose logs
bash:ls -labash:ls *bash:ls, bash:ls src/
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)

InputResultWhy
bash:git (bare, no subcommand)bash:git (literal)A globbed prefix would auto-approve every git subcommand
bash:cd /tmp && rm xliteralA &&/|/;/$()/> 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/hostsread:/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:
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

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

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.
DoomLoopDetector flags repeated identical tool calls — reset or change strategy when is_loop is true.

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.
# 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
SubcommandDescription
listList 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
resetReset all rules to factory defaults
export <file>Export all rules to a JSON file
import-rules <file>Import rules from a JSON file

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