git status and future git status -s runs never re-prompt.
Quick Start
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.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.Inspect the suggested pattern before saving
pattern= to override the derived pattern with any string you prefer.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.
| Situation | Behaviour |
|---|---|
Bare command (bash:git) | Kept literal — approving git alone never auto-approves git push. |
Compound command (bash:cd /tmp && rm x) | Kept literal — second op never swallowed into the reusable scope. |
Pipe / redirect / substitution (|, >, $(...)) | Kept literal. |
Non-shell target (read:, write:, edit:, …) | Unchanged — only bash:/shell: are generalised. |
Already-globbed target (bash:git *) | Unchanged. |
scope="once" | Always literal, even with reusable_scope=True. |
User-authored bash:rm * glob | Keeps exact fnmatch semantics — a bare bash:rm still does not match. |
PersistentApproval has derived=True, which enables a bare-prefix fallback in matches(): bash:git status * (derived) also matches the bare bash:git status.
| Situation | Stored pattern | Matches |
|---|---|---|
reusable_scope=False (default) | Literal bash:git status -s | Only that exact command |
reusable_scope=True, known command | Derived bash:git status * | Any git status … — including bare git status |
reusable_scope=True, bare command (bash:git) | Literal bash:git | Only bare git — never all git subcommands |
reusable_scope=True, compound (cd /tmp && rm x) | Literal (unchanged) | Only that compound command |
reusable_scope=True, non-shell (read:/etc/hosts) | Literal (unchanged) | Non-shell prefixes are never generalised |
pattern="bash:git *" explicit override | User’s exact string | derived=False — exact fnmatch semantics, no bare-prefix fallback |
The Arity Table
arity.derive_pattern uses this built-in table to decide how many leading tokens to keep as the reusable prefix.
| Category | Commands | Arity | Example |
|---|---|---|---|
| Version control | git, gh, hg, svn | 2 | git status -s → bash:git status * |
| Package managers | npm, yarn, pnpm, pip, cargo, go, poetry, uv, make | 2 | npm run build → bash:npm run * |
| Containers | docker | 2 | docker pull ubuntu → bash:docker pull * |
| Containers | docker compose | 2 | docker compose up -d → bash:docker compose * |
| Containers | kubectl, helm | 2 | kubectl get pods → bash:kubectl get * |
| Python tooling | python, python3 | 2 | python -m pytest → bash:python -m * |
| Python tooling | ruff | 2 | ruff check . → bash:ruff check * |
| Python tooling | pytest | 1 | pytest tests/ → bash:pytest * |
| System | apt, apt-get, brew, systemctl | 2 | apt install pkg → bash:apt install * |
| Unknown / other | — | 1 (conservative) | foo bar baz → bash:foo * (only if there are trailing args) |
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:| Condition | Example target | Stored as |
|---|---|---|
| Non-shell target | read:/etc/hosts | read:/etc/hosts |
| Already globbed | bash:git status * | bash:git status * |
Contains &&, ||, |, ;, & | bash:cd /tmp && rm -rf x | exact string |
Contains $(, backtick, >, <, newline | bash:echo $(whoami) | exact string |
| Bare single token equals the full command | bash:git | bash:git |
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()
| Parameter | Type | Default | Description |
|---|---|---|---|
target | str | — | The target that was approved/denied (e.g. "bash:git status -s") |
approved | bool | — | Whether approved (True) or denied (False) |
scope | str | "once" | "once", "session", or "always" |
agent_name | str | None | None | Scope to a specific agent |
reusable_scope | bool | False | When True and scope is session/always, derive a reusable prefix glob |
pattern | str | None | None | Explicit pattern — overrides target and reusable_scope |
PermissionManager.suggest_scope_pattern()
arity.derive_pattern. Returns the derived glob pattern, or the original target unchanged if derivation is not possible.
PersistentApproval.derived
| Field | Type | Default | Description |
|---|---|---|---|
derived | bool | False | True when the pattern was auto-generated by derive_pattern(). Enables the bare-prefix fallback in matches(). Loaded from approvals.json — older files default to False (backward compatible). |
PermissionManager API Reference
Full
PermissionManager configuration and rule optionsRelated
Interactive Approval
Terminal-prompt approval flow —
[A] persists a narrow command-prefix rule, [T] grants blanket tool accessDeclarative 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

