Permissions
Feature Definition
Section titled “Feature Definition”An AI coding agent has broad power: it can read files, write files, execute shell commands, make network requests, and commit to Git. Without a permission system, every one of those actions runs unchecked. The permission problem has three hard sub-problems. First, granularity: a blanket “allow all” or “deny all” is useless for real work, so the system must distinguish between reading a .env file versus reading a .ts file, or running ls versus running rm -rf /. Second, fatigue: prompting the user for every single operation makes the agent unusable, so the system needs batch approval, pattern-based allow-listing, and session memory. Third, safety floor: certain operations (shell execution, writing outside the workspace) are dangerous enough that even an explicit “auto-approve” flag should not silently grant them without the user understanding what they are consenting to.
This page covers the user-facing permission model and approval UX across all three reference implementations. For the OS-level sandbox enforcement (bubblewrap, Seatbelt, seccomp), see Platform Isolation. For the per-tool sandbox policy tiers, see Sandbox Modes. For tool-level prompt interaction details (modal flow, key bindings, batch handling), see Approval Flow.
Aider Implementation
Section titled “Aider Implementation”Repository: references/aider/ | Pinned SHA: b9050e1d
Aider’s permission model is the simplest of the three. There is one universal gate function, a global override flag, and a handful of per-feature toggles. No configuration DSL, no per-tool pattern matching, no persistent rule storage.
The confirm_ask() Universal Gate
Section titled “The confirm_ask() Universal Gate”Every user-facing permission check in Aider flows through a single method on the InputOutput class.
File: aider/io.py:807-815
@restore_multilinedef confirm_ask( self, question, default="y", subject=None, explicit_yes_required=False, group=None, allow_never=False,):The parameters control the behavior:
| Parameter | Purpose | Example |
|---|---|---|
question | Prompt text shown to user | "Create new file?" |
default | What happens if user presses Enter | "y" or "n" |
subject | The object being confirmed (path, command, URL) | "/src/main.py" |
explicit_yes_required | When True, bypasses the --yes-always flag — forces interactive confirmation | Shell commands |
group | Batch multiple confirmations with “All”/“Skip all” | Multiple shell commands |
allow_never | Enables “Don’t ask again” response | URL additions, file mentions |
The prompt is rendered with dynamic options based on context (io.py:831-846):
Run shell command?python -m pytest tests/ (Y)es/(N)o/(A)ll/(S)kip all/(D)on't ask again [Yes]: _The --yes-always Global Override
Section titled “The --yes-always Global Override”File: aider/args.py:760-764
The --yes-always flag sets InputOutput.yes = True. When the gate fires, it checks this flag first (io.py:866-871):
if self.yes is True: res = "n" if explicit_yes_required else "y"elif self.yes is False: res = "n"elif group and group.preference: res = group.preferenceelse: # Interactive prompt to userThe critical design decision: when explicit_yes_required=True and --yes-always is active, the answer is "n" (auto-reject), not "y". This means shell commands cannot be auto-approved — even in full auto mode, shell execution is blocked unless a human explicitly types “yes”. This is the one hardened gate in Aider’s entire permission model.
Where confirm_ask() Is Called
Section titled “Where confirm_ask() Is Called”Every gated operation in Aider and the flag it uses:
| Operation | File | explicit_yes_required | allow_never | group |
|---|---|---|---|---|
| Create new file | base_coder.py:2206 | No | No | No |
| Edit unadded file | base_coder.py:2226 | No | No | No |
| Run shell command | base_coder.py:2450 | Yes | Yes | Yes |
| Add command output to chat | base_coder.py:2479 | No | Yes | No |
| Add URL to chat | base_coder.py:971 | No | Yes | Yes |
| Add mentioned file to chat | base_coder.py:1770 | No | Yes | Yes |
| Fix lint errors | base_coder.py:1599 | No | No | No |
| Fix test errors | base_coder.py:1616 | No | No | No |
| Accept architect edits | architect_coder.py:17 | No | No | No |
| Create git repo | main.py:122 | No | No | No |
| Update .gitignore | main.py:191 | No | No | No |
| Proceed past context overflow | base_coder.py:1415 | No | No | No |
Batch Permissions via ConfirmGroup
Section titled “Batch Permissions via ConfirmGroup”File: aider/io.py:81-89
@dataclassclass ConfirmGroup: preference: str = None show_group: bool = True
def __init__(self, items=None): if items is not None: self.show_group = len(items) > 1When multiple related operations (e.g., three shell commands from one LLM turn) need confirmation, they are grouped. If the user selects “All”, group.preference = "all" is set and all remaining items in the batch auto-approve. “Skip all” does the inverse. Groups are scoped to a single batch — they don’t persist.
Session-Level “Never” Memory
Section titled “Session-Level “Never” Memory”File: aider/io.py:269, 821-825, 902-906
self.never_prompts = set()
question_id = (question, subject)if question_id in self.never_prompts: return False # Skip prompt entirely
# When user selects "D":if res == "d" and allow_never: self.never_prompts.add(question_id) return FalseThe never_prompts set is keyed by (question, subject) tuples. Selecting “Don’t ask again” for ("Add URL to the chat?", "https://example.com") suppresses future prompts for that exact URL. The set lives in memory only — it resets between sessions. Related sets: self.rejected_urls and self.ignore_mentions on base_coder.py:350-358.
Feature Toggle Flags (Not Permission Gates)
Section titled “Feature Toggle Flags (Not Permission Gates)”Several auto_* flags control whether features run at all, but they are not permission gates — they don’t prompt the user; they silently skip the feature:
| Flag | Default | What it disables |
|---|---|---|
--no-auto-commits | Off | Git commits after LLM edits |
--no-auto-lint | Off | Linting after edits |
--no-auto-test | Off | Test execution after edits |
--no-dirty-commits | Off | Pre-turn commit of user’s changes |
--no-auto-accept-architect | Off | Automatic acceptance of architect edits |
Silent Operations
Section titled “Silent Operations”Git commits when auto_commits=True run without any user prompt (base_coder.py:2376). Commit messages are LLM-generated without review (repo.py:326-373). There is no permission gate between the agent deciding to commit and the commit happening.
Codex Implementation
Section titled “Codex Implementation”Repository: references/codex/codex-rs/ | Pinned SHA: 4ab44e2c5
Codex has the most layered permission system: a two-axis policy model (approval policy x sandbox policy), a trait-based per-tool approval interface, a Starlark-based execpolicy DSL, a TUI approval overlay with rich UI, and per-session caching with metrics.
Two-Axis Policy Model
Section titled “Two-Axis Policy Model”Codex separates when to ask from what is allowed at the OS level.
Axis 1: AskForApproval enum — protocol/src/protocol.rs:359-382
pub enum AskForApproval { UnlessTrusted, // Only auto-approve is_safe_command() list OnFailure, // Run in sandbox; ask only if sandbox blocks it (deprecated) #[default] OnRequest, // Model-driven: ask when sandbox would restrict Never, // No approval ever; failures go to model}Axis 2: SandboxPolicy enum — protocol/src/protocol.rs:503-564
pub enum SandboxPolicy { DangerFullAccess, // No restrictions ReadOnly { access }, // Read-only with optional restrictions ExternalSandbox { network_access }, // Already in external container WorkspaceWrite { // Read + write to cwd, /tmp, configured roots writable_roots, read_only_access, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, },}These two axes combine into a decision matrix implemented in core/src/exec_policy.rs:407-469:
| Approval Policy | ReadOnly | WorkspaceWrite | DangerFullAccess | ExternalSandbox |
|---|---|---|---|---|
UnlessTrusted | Prompt | Prompt | Prompt | Prompt |
OnFailure | Allow (sandbox) | Allow (sandbox) | Allow | Allow |
OnRequest | Prompt | Prompt if escalated | Allow | Allow |
Never | Allow (sandbox) | Allow (sandbox) | Allow | Allow |
Approval Presets
Section titled “Approval Presets”File: utils/approval-presets/src/lib.rs
Three ready-made presets bundle the two axes:
| Preset ID | Label | Approval | Sandbox |
|---|---|---|---|
read-only | Read Only | OnRequest | ReadOnly |
auto | Default | OnRequest | WorkspaceWrite |
full-access | Full Access | Never | DangerFullAccess |
Users select a preset at startup (or via config), and the two axis values are set accordingly.
The Exec Policy Decision Flow
Section titled “The Exec Policy Decision Flow”File: core/src/exec_policy.rs:161-230
When a command needs execution, create_exec_approval_requirement_for_command() runs:
- Parse the command (handle
bash -lc "..."shell wrapping) - Check against
.rulesfiles — the execpolicy DSL (Starlark-based, stored in~/.codex/rules/or.codex/rules/) - If no rule matches, fall through to
render_decision_for_unmatched_command()which applies heuristics:- If
is_known_safe_command()(cat, ls, echo, find, grep, etc.) → Allow - If
command_might_be_dangerous()(python, bash, git, node, sudo, etc.) → Prompt - Otherwise, apply the approval policy matrix above
- If
- Generate a proposed
ExecPolicyAmendment— a prefix rule to allow future similar commands if approved
The Approvable Trait
Section titled “The Approvable Trait”File: core/src/tools/sandboxing.rs:192-235
Each tool implements a trait that plugs into the approval system:
pub trait Approvable<Req> { type ApprovalKey: Hash + Eq + Clone + Debug + Serialize; fn approval_keys(&self, req: &Req) -> Vec<Self::ApprovalKey>; fn should_bypass_approval(&self, policy: AskForApproval, already_approved: bool) -> bool; fn exec_approval_requirement(&self, req: &Req) -> Option<ExecApprovalRequirement>; fn start_approval_async<'a>(&'a mut self, req: &'a Req, ctx: ApprovalCtx<'a>) -> BoxFuture<'a, ReviewDecision>;}This trait lets each tool define its own approval keys (what to cache), whether to bypass approval (e.g., if already approved for session), and how to request approval asynchronously.
Approval Caching
Section titled “Approval Caching”File: core/src/tools/sandboxing.rs:30-107
pub struct ApprovalStore { map: HashMap<String, ReviewDecision>,}The with_cached_approval() helper checks if all approval keys for a request are already approved for the session. If so, it skips the prompt. If not, it fires the approval request, and on ApprovedForSession, caches the keys for the remainder of the session.
Metrics are tracked: Counter: "codex.approval.requested" with tags (tool, approved_status).
TUI Approval Overlay
Section titled “TUI Approval Overlay”File: tui/src/bottom_pane/approval_overlay.rs:40-173
The TUI presents a modal overlay for three types of approval:
pub enum ApprovalRequest { Exec { id, command, reason, network_approval_context, proposed_execpolicy_amendment }, ApplyPatch { id, reason, cwd, changes }, McpElicitation { server_name, request_id, message },}For Exec, the prompt reads: “Would you like to run the following command?” with the command displayed.
For ApplyPatch, it reads: “Would you like to make the following edits?” with a diff view.
For McpElicitation, it reads: “{server_name} needs your approval.”
Review Decision Types
Section titled “Review Decision Types”pub enum ReviewDecision { Approved, // One-time ApprovedForSession, // Cache for session ApprovedExecpolicyAmendment { amendment }, // Approve + add rule Denied, Abort, // Cancel current turn}ApprovedExecpolicyAmendment is unique to Codex: it approves the operation and writes a new execpolicy rule so future identical commands auto-approve. This lets the permission set grow organically during a session.
Network Approval
Section titled “Network Approval”File: core/src/tools/network_approval.rs:22-282
Network access has its own approval flow. When a sandboxed command tries to reach a host and the sandbox blocks it:
NetworkApprovalServicecheckssession_approved_hosts- If not cached, the command approval is augmented with a
NetworkApprovalContext { host, protocol } - TUI shows: “Do you want to approve access to “{host}”?”
- On approval, the host is cached for the session
Protected Paths
Section titled “Protected Paths”File: protocol/src/protocol.rs:749-795
In WorkspaceWrite mode, certain paths are automatically read-only even within writable roots:
let read_only_subpaths = vec![ writable_root.join(".agents"), writable_root.join(".codex"), writable_root.join(".git"), // also handles worktrees and submodules];The WritableRoot::is_path_writable() method checks both that a path is under a writable root and not under a protected subpath.
OpenCode Implementation
Section titled “OpenCode Implementation”Repository: references/opencode/packages/opencode/src/ | Pinned SHA: 7ed449974
OpenCode has the most configurable permission system: a ternary action model, wildcard pattern matching, a command arity dictionary for bash normalization, three-tier permission merging, session-scoped “always” memory with retroactive application, doom loop detection, and a rich TUI with diff previews.
Ternary Action Model
Section titled “Ternary Action Model”File: permission/next.ts:25-28
export const Action = z.enum(["allow", "deny", "ask"])Every permission evaluation resolves to one of three states:
allow: Tool executes immediately, no promptdeny: Tool is blocked, raisesDeniedErrorwith the matching rulesask: TUI prompts the user with once/always/reject options
Rule-Based Configuration
Section titled “Rule-Based Configuration”File: permission/next.ts:30-44
export const Rule = z.object({ permission: z.string(), // Tool name: "edit", "bash", "read" pattern: z.string(), // Glob pattern: "*.env", "npm run *" action: Action, // allow/deny/ask})Rules are evaluated with last-match-wins semantics (permission/next.ts:239-241):
const match = merged.findLast( (rule) => Wildcard.match(permission, rule.permission) && Wildcard.match(pattern, rule.pattern),)return match ?? { action: "ask", permission, pattern: "*" }If no rule matches, the default is "ask".
Wildcard Pattern Engine
Section titled “Wildcard Pattern Engine”File: util/wildcard.ts:4-17
export function match(str: string, pattern: string) { let escaped = pattern .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*/g, ".*") .replace(/\?/g, ".") if (escaped.endsWith(" .*")) { escaped = escaped.slice(0, -3) + "( .*)?" } return new RegExp("^" + escaped + "$", "s").test(str)}A trailing " *" (space + wildcard) is made optional — so "ls *" matches both "ls" and "ls -la". This matters for bash command patterns.
Bash Command Arity
Section titled “Bash Command Arity”File: permission/arity.ts:25-162
To normalize bash commands into meaningful approval patterns, OpenCode uses a command arity dictionary:
const ARITY: Record<string, number> = { cat: 1, // "cat" alone git: 2, // "git checkout" "git config": 3, // "git config user.name" npm: 2, // "npm install" "npm run": 3, // "npm run dev" cargo: 2, // "cargo build" docker: 2, // "docker run" "docker compose": 3, // "docker compose up" // ... 100+ entries}When the bash tool fires, it calls BashArity.prefix(command) to extract the meaningful prefix. For npm run dev --watch, the prefix is ["npm", "run", "dev"], and the “always” pattern becomes "npm run dev *". This way, approving npm run dev once covers npm run dev --watch, npm run dev --port 3000, etc.
Three-Tier Permission Cascade
Section titled “Three-Tier Permission Cascade”File: agent/agent.ts:51-100
Permissions merge from three levels, in order:
-
Global defaults (hardcoded safety rules):
{"*": "allow",doom_loop: "ask",external_directory: { "*": "ask" },question: "deny",read: { "*": "allow", "*.env": "ask", "*.env.*": "ask", "*.env.example": "allow" },} -
User config (from
opencode.json):{"permission": {"bash": { "npm *": "allow", "git *": "allow", "*": "ask" },"edit": "ask","external_directory": "deny"}} -
Agent-specific overrides (per agent type):
- The Explore agent gets restricted permissions:
"*": "deny"with onlygrep,glob,bash,readallowed - The Build agent gets
question: "allow"added
- The Explore agent gets restricted permissions:
The merge uses PermissionNext.merge(), and the last rule wins within each tier.
Permission Request Lifecycle
Section titled “Permission Request Lifecycle”File: permission/next.ts:68-161
When a tool calls ctx.ask():
await ctx.ask({ permission: "edit", patterns: [filepath], always: ["*"], // What pattern to remember if "always" selected metadata: { filepath, diff },})The ask() function evaluates the merged ruleset:
- If any pattern resolves to
"deny"→ throwsDeniedError - If any pattern resolves to
"ask"→ publishes to the TUI viaBus.publish(Event.Asked, ...)and returns aPromise<void>that blocks until the user responds - If all patterns resolve to
"allow"→ returns immediately
User Response Types
Section titled “User Response Types”File: permission/next.ts:89-90
export const Reply = z.enum(["once", "always", "reject"])once: Allow this specific operation. No memory.always: Allow this pattern permanently for the session. Adds rules to the approved set and retroactively resolves any other pending permissions that now match.reject: Block the operation. The user can optionally provide a text message explaining why, which becomes aCorrectedErrorsent back to the model as guidance.
Session-Scoped “Always” with Retroactive Application
Section titled “Session-Scoped “Always” with Retroactive Application”File: permission/next.ts:200-225
When the user selects “always”:
for (const pattern of existing.info.always) { s.approved.push({ permission: existing.info.permission, pattern, action: "allow", })}
// Retroactively resolve other pending permissionsfor (const [id, pending] of Object.entries(s.pending)) { const ok = pending.info.patterns.every( (pattern) => evaluate(pending.info.permission, pattern, s.approved).action === "allow", ) if (ok) { delete s.pending[id] pending.resolve() }}This retroactive resolution is important: if the agent fires three edit calls in rapid succession and the user approves the first with “always”, the second and third resolve automatically without prompting.
Approved patterns live in memory only. The code has a TODO comment (permission/next.ts:227-230) noting that persistent storage to SQLite is planned but not yet implemented.
Doom Loop Detection
Section titled “Doom Loop Detection”File: session/processor.ts:20, 152-176
When the agent calls the same tool with identical inputs three times in a row (DOOM_LOOP_THRESHOLD = 3), the processor fires a special permission request:
await PermissionNext.ask({ permission: "doom_loop", patterns: [value.toolName], metadata: { tool: value.toolName, input: value.input }, always: [value.toolName], ruleset: agent.permission,})The doom loop permission is configured as "ask" by default. If rejected, the session breaks (unless experimental.continue_loop_on_deny is set).
TUI Approval Rendering
Section titled “TUI Approval Rendering”File: cli/cmd/tui/routes/session/permission.tsx:119-299
The TUI renders tool-specific approval prompts:
- Edit operations: Show a unified or split diff of the proposed changes
- Bash commands: Show the command with
$prefix and description - Doom loops: Show “Continue after repeated failures” with a loop icon
- External directories: Show the directory path being accessed
Navigation: Left/Right arrows (or h/l) to select option, Enter to confirm, Escape to reject, Ctrl+F for fullscreen diff view.
Tool Pre-Filtering
Section titled “Tool Pre-Filtering”File: permission/next.ts:245-257
Tools that are "deny": "*" in the ruleset are removed from the LLM’s tool list entirely at setup time:
export function disabled(tools: string[], ruleset: Ruleset): Set<string> { const result = new Set<string>() for (const tool of tools) { const rule = ruleset.findLast((r) => Wildcard.match(permission, r.permission)) if (rule?.pattern === "*" && rule.action === "deny") result.add(tool) } return result}This prevents the model from even attempting to call a forbidden tool, saving a round trip.
Claude Code Implementation
Section titled “Claude Code Implementation”Source: Public documentation at code.claude.com and platform.claude.com (closed source, docs-inferred)
Claude Code has the most structured permission system of any reference: a tiered tool classification, deny-first rule evaluation, five permission modes, a four-step evaluation pipeline, five-level settings precedence, managed enterprise controls, and persistent bash command approvals.
Tiered Tool Classification
Section titled “Tiered Tool Classification”Every built-in tool falls into one of three tiers:
| Tool Tier | Examples | Approval Required | ”Don’t ask again” Scope |
|---|---|---|---|
| Read-only | File reads, Grep, Glob | No | N/A |
| Bash commands | Shell execution | Yes | Permanent per project directory + command |
| File modification | Edit, Write | Yes | Until session end |
The critical distinction: bash command approvals persist to disk permanently for the project+command pair, while file modification approvals are session-scoped. This is unique among all reference implementations. Aider, Codex, and OpenCode all scope approvals to the current session (Codex’s execpolicy amendments can optionally persist, but it is per-approval opt-in).
Rule Evaluation Order: deny -> ask -> allow
Section titled “Rule Evaluation Order: deny -> ask -> allow”Rules are evaluated in deny-first order:
- Check deny rules first (block regardless of other rules)
- Check allow rules (permit if matched)
- Check ask rules (prompt for approval)
- First matching rule at each tier wins
This is a significant departure from OpenCode’s last-match-wins semantics and Codex’s two-axis matrix. The deny-first approach means deny rules always take precedence regardless of where they appear in the configuration, which is more intuitive for security but less flexible for complex override patterns.
Example configuration:
{ "permissions": { "allow": [ "Bash(npm run *)", "Bash(git commit *)" ], "deny": [ "Bash(git push *)" ] }}Here, git push is always denied even though git * patterns could theoretically match via the allow rules.
Permission Modes
Section titled “Permission Modes”Five modes, settable at startup via --permission-mode or dynamically mid-session:
| Mode | Description |
|---|---|
default | Standard: prompts on first use of each tool |
acceptEdits | Auto-accept file edits and filesystem commands (mkdir, touch, rm, mv, cp) |
plan | Read-only: Claude analyzes but cannot modify or execute |
dontAsk | Auto-deny tools unless pre-approved via rules |
bypassPermissions | Skip all permission checks (requires isolated environment) |
dontAsk vs bypassPermissions distinction: dontAsk silently denies any tool not explicitly allowed by rules — it is a lockdown mode where only pre-approved operations proceed. bypassPermissions skips all checks entirely — everything is auto-approved. The difference is the direction of the default: dontAsk defaults to deny, bypassPermissions defaults to allow.
Subagent inheritance: bypassPermissions propagates to ALL subagents unconditionally and cannot be overridden per-subagent. This is deliberate but dangerous — subagents may have different system prompts and less constrained behavior. Managed settings can disable this mode entirely.
Dynamic switching: The Agent SDK supports changing permission mode mid-session via set_permission_mode(). This enables patterns like “start restrictive, loosen after reviewing the agent’s approach.”
Rule Syntax
Section titled “Rule Syntax”Rules follow the format Tool or Tool(specifier):
Bash rules with glob patterns:
BashorBash(*)— match all bash commandsBash(npm run build)— exact matchBash(npm run *)— prefix match with word boundary (space before*)Bash(npm*)— prefix match without word boundaryBash(* --version)— suffix matchBash(git * main)— middle wildcard
Word boundary semantics: Space before * enforces a word boundary. Bash(ls *) matches ls -la but NOT lsof. Bash(ls*) without space matches both. This is more nuanced than OpenCode’s arity-based prefix system.
Shell operator awareness: Claude Code parses shell operators (&&, ||, ;, pipes). A rule like Bash(safe-cmd *) will NOT match safe-cmd && evil-cmd. This is a significant security improvement over pattern-matching systems that operate on raw command strings.
Read/Edit rules follow the gitignore specification with four path types:
| Pattern | Meaning | Example |
|---|---|---|
//path | Absolute from filesystem root | Read(//Users/alice/secrets/**) |
~/path | Home directory relative | Read(~/Documents/*.pdf) |
/path | Relative to settings file | Edit(/src/**/*.ts) |
path or ./path | Relative to CWD | Read(*.env) |
Uses gitignore * (single directory) vs ** (recursive across directories) semantics.
MCP rules: mcp__servername__toolname or mcp__servername__* for all tools from a server.
Task (subagent) rules: Task(AgentName) to control which subagents can be used. Adding Task(Explore) to the deny array disables the Explore agent.
Four-Step Evaluation Pipeline (Agent SDK)
Section titled “Four-Step Evaluation Pipeline (Agent SDK)”The Agent SDK reveals the full evaluation sequence:
- Hooks (PreToolUse) — custom shell commands that can allow, deny, or pass-through
- Permission rules (settings.json) — deny -> allow -> ask evaluation
- Permission mode — bypassPermissions, acceptEdits, dontAsk, etc.
- canUseTool callback — runtime approval from the host application
This is the most layered evaluation of any reference. Each step can short-circuit: if hooks allow, rules are skipped. If rules deny, mode is skipped. The callback is the final fallback when no earlier step resolved the decision.
Five-Level Settings Precedence
Section titled “Five-Level Settings Precedence”From highest to lowest priority:
- Managed settings — system-wide, admin-deployed, cannot be overridden
- CLI arguments —
--permission-mode,--allowedTools, etc. - Local project —
.claude/local-settings.json(gitignored, per-developer) - Shared project —
.claude/settings.json(checked into VCS) - User settings —
~/.claude/settings.json
If a permission is allowed in user settings but denied in project settings, the project setting wins.
Managed Settings (Enterprise Controls)
Section titled “Managed Settings (Enterprise Controls)”System-wide policy files deployed by IT administrators to non-user paths:
| Platform | Path |
|---|---|
| macOS | /Library/Application Support/ClaudeCode/managed-settings.json |
| Linux/WSL | /etc/claude-code/managed-settings.json |
| Windows | C:\Program Files\ClaudeCode\managed-settings.json |
Settings only effective in managed context:
| Setting | Purpose |
|---|---|
disableBypassPermissionsMode | Prevent bypassPermissions mode and --dangerously-skip-permissions |
allowManagedPermissionRulesOnly | Block user/project permission rules; only managed rules apply |
allowManagedHooksOnly | Block user/project/plugin hooks; only managed + SDK hooks allowed |
strictKnownMarketplaces | Control which plugin marketplaces users can add |
No other reference implementation has anything equivalent to this enterprise control surface.
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”Approval fatigue is the primary failure mode. If the agent needs to edit 15 files in a single turn and each one prompts, the user will either switch to auto-approve (defeating the purpose) or abandon the session. Aider’s batch groups and OpenCode’s retroactive “always” resolution both address this, but neither is perfect — Aider’s groups only work within a single batch, and OpenCode’s pattern memory resets between sessions.
Shell commands are the highest-risk gate. All three implementations recognize this: Aider uses explicit_yes_required, Codex uses the exec policy DSL with dangerous-command heuristics, and OpenCode uses tree-sitter parsing plus the arity dictionary. The hard lesson from Aider’s design is that --yes-always auto-rejecting shell commands (returning “no” instead of “yes”) is surprising to users who expect it to approve everything.
Pattern specificity is tricky. OpenCode’s "npm run *" pattern looks clean, but edge cases abound: npm run "build && rm -rf /" matches "npm run *". Codex avoids this by classifying entire commands as safe or dangerous rather than pattern-matching substrings. OpenCode mitigates it partially through tree-sitter bash parsing, but the arity system operates on token splitting, not AST analysis.
Network approval is under-covered. Only Codex has explicit network approval with per-host caching. Aider has no network gating at all. OpenCode has no OS-level network restriction. For agents that can fetch URLs or install packages, network access is a significant attack surface.
Persistent approval is deliberately avoided (except by Claude Code). Aider and OpenCode reset approval state between sessions. Codex’s execpolicy amendments can persist to disk, but it is opt-in per approval. Claude Code breaks this pattern by persisting bash command approvals permanently per project+command. The risk of persisting approvals is that a compromised session could write permissive rules that affect future sessions — Claude Code accepts this risk for bash commands specifically, betting that the convenience outweighs the threat model for commands a user has explicitly approved.
Rule evaluation order has security implications. OpenCode’s last-match-wins allows later rules to override earlier denials, which is powerful for layered configs but means a misconfigured later rule can silently permit dangerous operations. Claude Code’s deny-first approach (deny -> allow -> ask) guarantees that deny rules always win regardless of ordering, which is more intuitive for security. Codex sidesteps the issue entirely with its two-axis matrix. The choice between evaluation orders affects how users reason about their permission configs.
Shell operator injection defeats naive pattern matching. A rule like Bash(safe-cmd *) looks safe, but safe-cmd && rm -rf / matches naive glob patterns. Claude Code addresses this by parsing shell operators — the rule won’t match commands joined by &&, ||, ;, or pipes. OpenCode partially mitigates via tree-sitter bash parsing but applies arity at the token level, not the AST level. This is a real attack vector for agents with user-facing pattern rules.
Enterprise lockdown requires a separate settings layer. Claude Code’s managed settings (system-wide, admin-deployed, cannot be overridden) solve a real enterprise problem: ensuring that developer-configured rules cannot weaken organizational policy. No other reference implementation has this. The allowManagedPermissionRulesOnly setting goes further by preventing ALL user/project rules, ensuring only IT-approved rules apply.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Architecture
Section titled “Architecture”OpenOxide will implement a rule-based permission system combining the best patterns from all three references:
Core types (Rust):
pub enum PermissionAction { Allow, Deny, Ask,}
pub struct PermissionRule { pub permission: String, // Tool name or glob pub pattern: String, // File path or command glob pub action: PermissionAction,}
pub type Ruleset = Vec<PermissionRule>;Evaluation: Deny-first semantics (following Claude Code). Rules are organized into three arrays: deny, allow, ask. Deny rules are checked first, then allow, then ask. First match within each tier wins. Default to Ask when no rule matches. This is a departure from the original OpenCode-inspired last-match-wins plan — the deny-first approach is more intuitive for security (deny always wins regardless of ordering) and aligns with how users expect permission systems to work.
Crates
Section titled “Crates”openoxide-permission: Core rule evaluation, pattern matching, approval store. Depends onglobsetfor glob matching (not regex conversion like OpenCode —globsethandles edge cases better).openoxide-permission-tui: ratatui-based approval overlay with diff rendering (following Codex’s overlay pattern). Keybindings:y/n/a(always)/s(skip all)/Esc(reject with message).
Key Design Decisions
Section titled “Key Design Decisions”-
Deny-first evaluation (Claude Code pattern): Rules organized into
deny,allow,askarrays. Deny rules checked first, then allow, then ask. This replaces the earlier plan for last-match-wins (OpenCode pattern). Deny-first is more intuitive for security and matches how users reason about access control. -
Five-level settings precedence (Claude Code pattern): Managed (system-wide) → CLI arguments → local project (gitignored) → shared project (VCS) → user config. Use
Vec<SettingsLayer>with a merge that respects precedence and deny-first evaluation within each layer. -
Four-step evaluation pipeline (Claude Code pattern): Hooks → Permission rules → Permission mode → Approvable callback. Each step can short-circuit. This replaces the simpler evaluate-then-prompt flow from the OpenCode-inspired design.
-
Session-scoped approval memory with retroactive resolution (OpenCode pattern) for file modifications. Persistent approval storage for bash commands (Claude Code pattern), scoped to project directory + command pattern. Store persistent approvals in
.openoxide/approvals.toml. -
Command arity system (OpenCode pattern) for bash normalization, plus shell operator awareness (Claude Code pattern). Parse
&&,||,;, and pipes before pattern matching sosafe-cmd && evil-cmddoes not matchsafe-cmd *rules. -
Dangerous command classification (Codex pattern) as a fallback. If no rule matches a shell command, check against a known-dangerous list (interpreters, package managers, git) and require explicit approval. Additionally, implement a command injection detector (Claude Code pattern) that overrides allow rules for suspicious commands.
-
Doom loop detection (OpenCode pattern). Track the last N tool calls with their inputs in a ring buffer. If the same
(tool, input_hash)appears 3 times consecutively, fire a"doom_loop"permission request. -
Tool pre-filtering (OpenCode pattern). Before sending the tool list to the LLM, remove tools that are
Deny("*")in the merged ruleset. -
Permission modes (Claude Code pattern): Support
default,accept_edits,plan,dont_ask, andbypass_permissionsmodes.bypass_permissionsinherits to all subagents. Support dynamic mode switching mid-session. -
Managed settings (Claude Code pattern): Support a system-wide config at
/etc/openoxide/managed.toml(Linux) that cannot be overridden. Supportdisable_bypass_permissions,allow_managed_rules_only, andallow_managed_hooks_onlyflags. -
Gitignore-compatible path patterns (Claude Code pattern) for Read/Edit rules: support
//path(absolute),~/path(home),/path(relative to config), andpath(relative to CWD).
Traits
Section titled “Traits”pub trait Approvable { fn permission_name(&self) -> &str; fn patterns(&self, args: &serde_json::Value) -> Vec<String>; fn always_patterns(&self, args: &serde_json::Value) -> Vec<String>; fn metadata(&self, args: &serde_json::Value) -> serde_json::Value;}Each tool implements Approvable to declare what permission it needs and what patterns to remember. The approval system calls permission_name() and patterns() to evaluate the ruleset, and always_patterns() to determine what to cache on “always” approval.