Skip to main content
Reusable scopes let one persistent approval cover every trailing-arg variant of the same command — approve git status and future git status -s runs never re-prompt.

Quick Start

1

Enable reusable scopes on your Agent

Add one line to opt in — approval fatigue drops immediately:
The first time the agent runs git status -s, the user sees the prompt once. Every subsequent git status <flags> variant is auto-approved for the session.
2

Preview and store scopes programmatically

For advanced workflows — CLIs, approval UIs, or testing — call the PermissionManager API directly:
suggest_scope_pattern() lets you surface the derived pattern in your UI before committing it, so users can review what always will cover.
3

Inspect the suggested pattern before saving

Pass pattern= to override the derived pattern with any string you prefer.
4

Persistence round-trip


How It Works

When reusable_scope=True and scope is "session" or "always", PermissionManager.approve() calls suggest_scope_pattern() internally. That delegates to arity.derive_pattern(), which looks up the command in a small arity table and builds a glob. The stored PersistentApproval has derived=True, which enables a bare-prefix fallback in matches(): bash:git status * (derived) also matches the bare bash:git status.

The Arity Table

arity.derive_pattern uses this built-in table to decide how many leading tokens to keep as the reusable prefix.
Multi-word keys win over single-word keys. docker compose (a 2-word key, arity 2) takes precedence over docker (arity 2), so docker compose up -d derives bash:docker compose *. Commands not in the table default to arity 1 — only the first token is kept.

Behaviour table

Escape hatches — these always stay literal: The bare-single-token rule is the most important: approving bash:git stays literal — it never automatically covers bash:git push --force.

Extend the arity table for your own tools

Pass a custom arity_map to derive_pattern() to add commands not in the built-in table:

What Never Gets Generalised

derive_pattern is conservative. These cases always return the literal target unchanged:
  • Non-shell targets — anything that doesn’t start with bash: or shell:.
  • Empty commandsbash: with no command string.
  • Existing globs — targets already containing * or ?.
  • Shell control operators — targets containing &&, ||, |, ;, &, $(, `, >, <, or a newline.
  • Bare single-token commandsbash:git, bash:ls — never become bash:git *.
This prevents a compound-command approval from silently expanding its scope to cover a second unrelated command.

Common Patterns

Approve once, cover all trailing-arg variants
Bare command stays literal
Compound commands stay literal

Session vs Always scope

Use session for exploratory work and always only for commands you trust unconditionally across all future sessions.

Preview before storing

Preview and override before saving
Custom arity map for project-specific commands

Best Practices

Routine VCS and build commands (git status, npm run, pytest) are the best candidates. They have stable subcommand prefixes and many trailing-arg variants.
Call suggest_scope_pattern() in your CLI or approval UI so users can see exactly what always will cover before confirming. A pattern like bash:rm * deserves a careful second look:
A bad derived pattern that uses always persists across all future sessions. Use session by default for reusable scopes — if the pattern turns out to be safe, upgrade it to always deliberately:
If suggest_scope_pattern returns a pattern that is broader or narrower than you want, override it with pattern=. The stored approval will have derived=False, keeping exact fnmatch semantics.
A cd /tmp && rm x approval covers only that exact string. If you want to reuse two operations independently, create two separate approvals — one for cd variants and one for rm variants.
Path approvals such as read:/etc/hosts or write:/tmp/report.txt must remain exact. File paths that look like prefixes (read:/etc/) would grant access to all files under /etc/, which is a security anti-pattern. The literal-only behaviour for non-shell targets is by design.
If your agent uses an internal tool (mytool) or a domain-specific CLI (kubectl), pass a custom arity_map to derive_pattern() so the prefix is derived correctly:
A deny rule on bash:rm * still wins. Reusable scopes only extend allow approvals — deny is enforced at the rule level. See Command-Aware Permissions.

API Reference

PermissionManager.approve()

PermissionManager.suggest_scope_pattern()

Exception-safe wrapper around arity.derive_pattern. Returns the derived glob pattern, or the original target unchanged if derivation is not possible.

PersistentApproval.derived

PermissionManager API Reference

Full PermissionManager configuration and rule options

Interactive Approval

Terminal-prompt approval flow — [a] always persists a narrow command-prefix rule using this scope system

Declarative Permissions

Pre-declare allow/deny rules in YAML, CLI, or Python

Permissions

Full PermissionManager Python SDK reference

Command-Aware Permissions

How compound shell commands are evaluated

Durable Approvals

Persist approvals across restarts

Workspace Boundary

Restrict agent file access to the project directory