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
pattern= to override the derived pattern with any string you prefer.4
Persistence round-trip
How It Works
Whenreusable_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 customarity_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:orshell:. - Empty commands —
bash:with no command string. - Existing globs — targets already containing
*or?. - Shell control operators — targets containing
&&,||,|,;,&,$(,`,>,<, or a newline. - Bare single-token commands —
bash:git,bash:ls— never becomebash:git *.
Common Patterns
Approve once, cover all trailing-arg variantsSession vs Always scope
session for exploratory work and always only for commands you trust unconditionally across all future sessions.
Preview before storing
Best Practices
Use reusable_scope=True for routine shell commands
Use reusable_scope=True for routine shell commands
Routine VCS and build commands (
git status, npm run, pytest) are the best candidates. They have stable subcommand prefixes and many trailing-arg variants.Preview the scope before storing
Preview the scope before storing
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:Prefer session over always for reusable scopes
Prefer session over always for reusable scopes
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:Pass an explicit pattern= to override the suggestion
Pass an explicit pattern= to override the suggestion
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.Compound approvals are intentionally narrow
Compound approvals are intentionally narrow
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.Non-shell targets stay literal — that is intentional
Non-shell targets stay literal — that is intentional
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.Extend ARITY for your own CLI tools
Extend ARITY for your own CLI tools
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:Not a replacement for deny rules
Not a replacement for deny rules
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()
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 optionsRelated
Interactive Approval
Terminal-prompt approval flow —
[a] always persists a narrow command-prefix rule using this scope systemDeclarative Permissions
Pre-declare allow/deny rules in YAML, CLI, or Python
Permissions
Full
PermissionManager Python SDK referenceCommand-Aware Permissions
How compound shell commands are evaluated
Durable Approvals
Persist approvals across restarts
Workspace Boundary
Restrict agent file access to the project directory

