Permission modes¶
Bernstein gives every spawned agent a permission mode that decides which tool calls run, which need approval, and which are blocked outright. You pick the mode once at startup; it stays fixed for the lifetime of the run and is applied consistently to every rule evaluation and approval gate.
This page is the operator's guide to choosing the right mode. If you just want a one-line answer:
- Local hacking, you trust the agent:
bypass - Read-only review of an agent's plan:
plan - Headless / CI / scheduled runs:
auto - Interactive run on a fresh repo:
default(this is the default)
The four modes at a glance¶
The modes form a strict ordering from most permissive to most restrictive. Critical rules are never relaxed in any mode.
| Mode | Rank | One-liner |
|---|---|---|
bypass | 0 | Skip all approvals. Only critical-severity rules still apply. |
plan | 1 | Enforce critical+high rules. Useful for dry-runs and plan reviews. |
auto | 2 | Enforce critical+high+medium rules. The non-interactive default. |
default | 3 | Enforce every rule, ask on anything ambiguous. The interactive default. |
bypass — most permissive¶
Only critical-severity rules are enforced. High/medium/low rules are relaxed to allow. The approval gate is skipped at task completion (bypass_enabled=True). This is the mode behind the legacy --dangerously-skip-permissions flag.
Use when: you're running on your own dev box, you trust the agent and the goal, and you want to see what it does without prompts. Do not use in CI, on shared machines, or against repos that contain secrets your agent shouldn't touch.
plan — review-friendly¶
Enforces critical and high rules, relaxes medium and low. Most destructive things are still gated, but quality-of-life prompts get out of the way.
Use when: you want to see an agent's plan and a small amount of exploratory tool use without the full approval ceremony. The legacy --plan flag and plan_mode: true config both map to this mode.
auto — the headless default¶
Enforces critical, high, and medium rules; only low-severity rules are relaxed. No legacy flag — this is what an orchestrator picks by default when no operator is at the keyboard.
Use when: a scheduled job, CI runner, or automation harness drives the orchestrator. The mode protects against the most common destructive mistakes while keeping prompts to a minimum.
default — the interactive default¶
Every rule is enforced as written. Tool calls that match no rule fall through to ask, escalating to a human prompt. Approval gates run as designed.
Use when: an operator is at the keyboard, the agent is new to the repo, or the goal involves anything reversible. This is the safest setting and what new users should start on.
When to use which — decision matrix¶
| Situation | Recommended mode |
|---|---|
| First time running an agent in a repo | default |
| You want to inspect the plan before any tool runs | plan |
| Headless run from CI / cron / scheduler | auto |
| Local dev box; you trust the agent and the goal | bypass |
| Repo holds secrets the agent should not touch | default |
| You hit "ask" prompts every few seconds and it's fine | default |
| You hit "ask" prompts every few seconds and it's not | step down to auto |
| You disabled all rules and still see prompts | check hooks (next section) |
Rule of thumb: pick the strictest mode that lets the agent finish without you intervening every minute. Climbing past auto should be a deliberate choice, not a reaction to friction.
How a tool call gets resolved¶
When an agent invokes a tool, Bernstein walks four steps. The mode participates in step 2.
- Match a rule.
PermissionRuleEnginewalks the rule list in declaration order. First match wins. No match falls through todefault_for_no_match(mode)(default→ask; everything else →allow). - Apply mode relaxation. The matched rule has an action (
allow/ask/deny) and a severity. The compatibility matrix below converts it into an effective action. - Resolve hooks.
PermissionResolutionMatrixcombines the effective action with whatever any hooks returned (allow/deny/neutral). Hooks can restrict anallow, but they cannot override adenyor bypass anask. - Apply outcome.
ALLOW→ tool executes.ASK→ human prompt (interactive only).DENY→ tool blocked.
Mode × severity → enforced?¶
| Mode | critical | high | medium | low |
|---|---|---|---|---|
bypass | enforced | relaxed | relaxed | relaxed |
plan | enforced | enforced | relaxed | relaxed |
auto | enforced | enforced | enforced | relaxed |
default | enforced | enforced | enforced | enforced |
Relaxed means the rule's action is overridden to allow. Critical rules are never relaxed, regardless of mode.
Hook resolution rules¶
Once you have an effective action and a hook outcome:
- Effective rule =
DENY→ DENY (hooks cannot override) - Effective rule =
ASK→ ASK (hooks cannot bypass humans) - Effective rule =
ALLOW+ hook =DENY→ DENY - Effective rule =
ALLOW+ hook =ALLOW/NEUTRAL→ ALLOW - No rule + hook =
DENY→ DENY - No rule + hook =
ALLOW→ ALLOW - No rule + hook =
NEUTRAL→ ASK (default to safety)
If a tool call surprises you, the resolution chain above is the shortest path to a useful answer.
Configuration¶
YAML (bernstein.yaml)¶
If absent or null, the orchestrator falls back to default and logs a warning when an unrecognised value is supplied.
CLI flag¶
Legacy flag mapping¶
The orchestrator still accepts older flags and quietly maps them:
| Legacy flag / config value | Canonical mode |
|---|---|
--dangerously-skip-permissions | bypass |
dangerously_skip_permissions: true | bypass |
--plan / plan_mode: true | plan |
--auto / no flag (orchestrator) | auto |
| (interactive CLI, no flag) | default |
resolve_mode() checks canonical names first, then legacy names, then falls back to default with a warning.
Worked example¶
You run an agent in auto mode. Your rules.yaml says:
- match:
tool: Bash
command: "rm -rf /"
action: deny
severity: critical
- match:
tool: Bash
command: "rm -rf *"
action: ask
severity: high
- match:
tool: Write
action: ask
severity: low
The agent calls:
Bash("rm -rf /")→ matches rule 1, severity critical, mode does not relax → DENY.Bash("rm -rf *")→ matches rule 2, severity high,autoenforces high → ASK the operator.Write("README.md", ...)→ matches rule 3, severity low,autorelaxes low → ALLOW.
Switch to default and rule 3 stays ask. Switch to bypass and rule 1 still denies, rule 2 relaxes to allow, rule 3 relaxes to allow.
Code pointers¶
| File | What it does |
|---|---|
src/bernstein/core/security/permission_mode.py | Canonical PermissionMode enum, resolve_mode(), compatibility matrix |
src/bernstein/core/security/permission_rules.py | PermissionRuleEngine — matches rules, applies mode relaxation |
src/bernstein/core/security/permission_matrix.py | PermissionResolutionMatrix — combines rule outcome with hook outcome |
src/bernstein/core/security/approval.py | Approval gate — honours bypass_enabled when mode is bypass |
tests/unit/test_permission_mode.py | 62 tests covering every cell of the matrix |
The full engineering spec lives at dev/specs/internal-workflows/WORKFLOW-permission-mode-hierarchy.md and is the single source of truth for the resolution rules.
Related¶
- Sandbox backends — what an agent can reach even when permissions allow a call.
operations/runbooks.md— automated remediation for the kinds of failures permissions are meant to prevent in the first place.