Approval Policy: Same Command, Two Knobs Decide Whether to Ask
Whether it asks by default is tied to the sandbox kind. After you say Yes once, what it remembers is the full command — or a prefix.
- Read this turn’s AskForApprovalprotocol.rs L924
- See whether FileSystemSandboxKind is Restrictedpermissions.rs L227
- Never gives Skip, UnlessTrusted gives NeedsApprovalsandboxing.rs L198
- OnRequest or Granular only ask when Restrictedsandboxing.rs L200
- Granular that must ask, with sandbox approval off, is Forbiddensandboxing.rs L209
- An execpolicy Prompt still clears a second gateexec_policy.rs L214
- Session cache only takes ApprovedForSession, keyed exactlysandboxing.rs L108
- When the policy changes, the model’s instructions change with itpermissions_instructions.rs L271
You leave approval on on-request and twist the sandbox from workspace-write to danger-full-access. Ten minutes ago that out-of-sandbox command still popped a window. Now it doesn’t. You didn’t touch the approval knob.
Approval policy AskForApproval is which ask-a-human rule this turn uses; the type has four variants. Filesystem sandbox kinds are three, defaulting to Restricted — read-only, or write the workspace only. Both knobs live in the same decision function. Twist one, and the other one’s behavior follows.Source:codex-rs/protocol/src/protocol.rs lines 924–947, and codex-rs/protocol/src/permissions.rs lines 227–232
A command first reads the approval policy, then whether the filesystem is Restricted, then lets execpolicy stack Allow, Prompt, or Forbidden on top. execpolicy is the rule file that gives a command text those three states.
Whether it asks by default lives in default_exec_approval_requirement. The Never layer gives Skip. The UnlessTrusted layer gives NeedsApproval. OnRequest and Granular only ask when Restricted. read-only and workspace-write are both Restricted, so those two cells share a default ending. danger-full-access turns kind into Unrestricted, and the default function takes Skip.Source:codex-rs/core/src/tools/sandboxing.rs lines 198–230
Granular has one more gate. When approval is needed and sandbox_approval is off, it goes straight to Forbidden. If the filesystem is already Unrestricted, needs_approval flips false first and the Forbidden branch cannot be entered. Turning sandbox approval off only blocks a window that was going to pop anyway.
When Never meets an execpolicy Prompt, the ask upgrades into a deny. It does not mean everything is allowed to run.Source:codex-rs/core/src/exec_policy.rs lines 214–236
untrusted cannot be written into config. The type is still there; the public string has retired. If a user writes it explicitly, the whole load fails. If they don’t write it, project trust decides: trusted takes OnRequest, explicitly untrusted takes UnlessTrusted.Source:codex-rs/core/src/config/mod.rs lines 3607–3625
There is one more hard reject at load time. If requirements forbid danger-full-access but config writes approval_policy = "never", the loader first falls the permission profile back to read-only. Read-only plus never-ask means the model sits in a narrow sandbox with no one to ask. That combo is illegal on the spot.Source:codex-rs/core/src/config/mod.rs lines 3969–3979
When the disk is fully writable, asking once more about leaving the sandbox is meaningless. Tie the two knobs together and you get fewer popups. The cost: changing the sandbox silently walks approval behavior away. Rewrite it in another language and you still have to answer first: in full-power mode, do you still ask a human?
The policy enum states the rules by exhaustive branches. Add a variant and the compiler forces every decision function to take a stand. That’s the type keeping runtime’s gate.
The two remember options on the popup look alike. One remembers this command for the session. The other writes a prefix into execpolicy and lasts across sessions. Click the wrong one and the later boundary questions come out backwards.
A regular exec popup’s default menu doesn’t even have ApprovedForSession. It only shows up with a network context. Ordinary commands get a one-shot approve; if there’s a prefix-amendment proposal, remember-by-prefix is added; last is cancel. The remember users hit most often is the prefix one.Source:codex-rs/protocol/src/approvals.rs lines 314–347
The session cache lives in ApprovalStore, same lifetime as the session. The key is the normalized full command, plus cwd, env, and an execpolicy fingerprint. A hit only happens when every key is already ApprovedForSession. A one-shot Approved never enters the map.Source:codex-rs/core/src/tools/sandboxing.rs lines 64–116
Approve npm run test into the session and npm run lint is another key. /bin/bash -lc and bash -lc cache as the same token list when a single plaintext command can be split out. The session cache keys on the full command, not npm *. Prefix widening only goes through execpolicy.
Changing the approval policy does not empty this map. The key has no AskForApproval. Switch mid-stream from OnRequest to UnlessTrusted and cached approvals still count. Changing execpolicy changes the fingerprint, and only then does the cache die.
The two remembers match two threats. An exact key cannot stop a changed flag. A prefix can, and it can also widen a wrapped command too far. Split the layers so each can carry its own deny-advice list. On a day with few popups, a one-shot allow still holds.
Changing the policy is not enough if you only change the runtime branch. The instructions the model sees must change in lockstep, or it will still ask for require_escalated under the old rule. Under Never, if the instructions still teach it to escalate, runtime upgrades the ask into a deny and the model just keeps hitting the wall.
The permission note is a developer message. Pick the sandbox template first, then the approval template. Never, UnlessTrusted, and OnRequest each have ready markdown. Never’s file is one line: don’t send sandbox_permissions again; the command will be refused. Granular has no fifth file — it builds the allow list and deny list on the five switches. Both notes are spliced into one block so the model sees the current combo at once.Source:codex-rs/prompts/src/permissions_instructions.rs lines 271–291
Hooks sit in front of the human. If a hook returns Allow or Deny, the popup never appears. A hook’s Allow is a one-shot Approved; it does not write the session cache.
Runtime and the model’s instructions are two sides of the same contract. Change the decision function and that dictionary line must change with it. Keep both in one module, feed the same tests to both sides, and the shape still holds in another language.
DSH: two knobs, a grant does not widen
DSH’s approval policy is only ask and never. never returns rejected before it is dispatched to an answerer; a later listener cannot change that promise. The grant is only allowed-once. No session cache, no prefix amendment. Ask once, ask again next time.Source:packages/interaction/user-approval/src/index.ts lines 84–94, and lines 304–312
Sandbox and approval are two independent knobs. The dropdown the user sees is a preset table: workspace-write tied to ask, danger-full-access tied to never. Picking the next gear calls the two setters separately. Off the table it shows custom. So DSH can leave approval on ask and twist the sandbox to full-disk write. Codex’s OnRequest defaults to Skip on Unrestricted; that combo cannot stand alone in the default function.Source:packages/interaction/permission-presets/src/index.ts lines 167–176
Claude Code: remember writes into the rule table
Claude Code’s outward permission mode is five gears, plus internal auto and bubble. Rule sources include userSettings, projectSettings, session, cliArg. The decision checks a whole-tool deny first, then walks down.Source:restored-src/src/types/permissions.ts lines 16–29, and restored-src/src/utils/permissions/permissions.ts lines 1169–1181
alwaysAllowRules can record allows by source; session is one of those gears. Next time it matches by rule, not full argv equality. The cost: the matcher must defend wrapping itself. Codex hands widening to execpolicy prefixes and a deny-advice list. dontAsk is close to Never. Codex has no public always-allow approval policy; Never is still caught by execpolicy.Source:restored-src/src/types/permissions.ts lines 54–62, and line 433
Twist the sandbox: is the popup still there?
Leave approval on on-request and twist the sandbox from workspace-write to danger-full-access. Run a command that writes a path outside the workspace. Is the popup still there? Why?
Then approve the same npm run test for the session, and submit npm run lint. Should the cache hit? If you clicked remember-by-prefix, does the answer change?