How do permissions work in Codex?
how do permissions work? "default permissions" sometimes asks me, sometimes it doesn't... what's the trick there?
Now I have the full picture. Here's the comprehensive answer:
##How Permissions Work in Codex
The permissions system has three layers that interact to decide whether you get prompted:
###Layer 1: Approval Policy (AskForApproval)
This is the top-level knob. It's determined by (in priority order):
- CLI override (
--approval-mode) - Config profile (
approval_policyin your profile) - Config file (
approval_policyincodex.toml) - Project trust level (auto-detected fallback)
The possible values are defined in codex-rs/protocol/src/protocol.rs:826:
| Policy | Behavior |
|---|---|
untrusted | Only "known safe" read-only commands auto-approved. Everything else prompts. |
on-failure | (deprecated) All commands auto-approved inside sandbox; prompt only if sandbox fails. |
on-request | (default) The model decides when to escalate. Normal commands run in the sandbox silently; only escalation requests prompt you. |
granular | Fine-grained control over individual approval flows (sandbox, rules, skills, MCP, etc.) |
never | Never prompt. Failures go straight back to the model. |
The auto-detection fallback (codex-rs/core/src/config/mod.rs:1727):
if active_project.is_trusted() {
AskForApproval::OnRequest
} else if active_project.is_untrusted() {
AskForApproval::UnlessTrusted
} else {
AskForApproval::default() // OnRequest
}
So if you've explicitly marked a project as untrusted, the policy quietly becomes UnlessTrusted (prompts for almost everything). Otherwise it defaults to OnRequest.
###Layer 2: Sandbox Policy + default_permissions
The default_permissions field in your config points to a named permissions profile under the [permissions] table. This determines the sandbox boundaries — what files the model can read/write and whether network is allowed.
From codex-rs/config/src/config_toml.rs:112:
default_permissions = "workspace"
[permissions.workspace]
filesystem = { ... }
network = { ... }
The three UI presets (codex-rs/utils/approval-presets/src/lib.rs) map to:
| Preset | Approval | Sandbox |
|---|---|---|
| Read Only | on-request | ReadOnly (restricted filesystem) |
| Default | on-request | WorkspaceWrite (restricted filesystem) |
| Full Access | never | DangerFullAccess (unrestricted) |
###Layer 3: The Actual Decision Logic — Why It Sometimes Asks and Sometimes Doesn't
This is where the "trick" is. The core logic lives in render_decision_for_unmatched_command() (line 547). Here's the decision tree for the default on-request policy:
-
Is the command "known safe"? (e.g.,
cat,ls,grep,head,wc,findwithout-delete, read-onlygitsubcommands, etc.) → ✅ Auto-approved, no prompt. (is_known_safe_command()) -
Is the command flagged as "potentially dangerous"? (e.g.,
rm, destructivegitcommands, etc.) → ⚠️ Always prompts (unless policy isnever). (command_might_be_dangerous()) -
Otherwise, for non-dangerous, non-safe commands:
- If filesystem sandbox is
Unrestricted(Full Access mode): → ✅ Auto-approved — you chose to trust everything. - If filesystem sandbox is
Restricted(Read Only or Default):- Did the model request
sandbox_permissions: "require_escalated"or"with_additional_permissions"? → ⚠️ Prompts you — it's asking to break out of the sandbox. - Did the model use default sandbox (
use_default)? → ✅ Auto-approved — the sandbox itself enforces restrictions, no need to prompt.
- Did the model request
- If filesystem sandbox is
-
Exec-policy rules (
.rulesfiles in~/.codex/rules/): If a command matches anallowrule, it's auto-approved (and can even bypass the sandbox). If it matches apromptrule, it always prompts. If it matches aforbiddenrule, it's rejected outright.
###TL;DR — "The Trick"
With the default permissions (on-request + workspace-write sandbox):
| Scenario | Prompted? | Why? |
|---|---|---|
cat foo.txt, ls, grep | No | Known-safe command |
npm test, cargo build (normal) | No | Not dangerous, sandbox handles it |
Model asks to npm install with require_escalated | Yes | Model explicitly requests escalation (needs network) |
rm -rf something | Yes | Flagged as dangerous |
Matches an allow rule in ~/.codex/rules/ | No | Pre-approved by your rules |
| You previously approved with "for session" | No | Cached approval in ApprovalStore |
The model itself decides when to set sandbox_permissions: "require_escalated" vs just running the command normally. The system prompt (on_request.md) instructs it to escalate when commands need network, write to unusual paths, or fail due to sandbox restrictions. When the model doesn't request escalation, and the command isn't dangerous, it just runs inside the sandbox — that's why you don't get prompted for most commands.
END OF POST