OpenAI Codex · Code Mode

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.

Course goalAfter this lesson you can explain two things. Why the same command swings from a straight allow to needing a human nod. And after you click Yes once, whether it asks again next time — what the system actually remembered.
Try it first · Same command, swap the policy, watch the ending
Four policies, three sandboxes: which cell stamps allow, ask, or deny
Cmd
The first three take Allow. The fourth hits execpolicy Prompt.
Policy
Sandbox
Knobs
Click a cell to jump there. The two remember modes are mutually exclusive.
Twelve-cell sandbox · read-only and workspace often stamp the sameThe two Restricted rows collide
CodexOnRequest · workspace-write
WaitHit Play to see which stamp the same command gets after you swap the policy.
Instructions for the modelNo policy chosen yet.
DSHask · workspace-write
WaitThe two knobs are independent. Full-disk write can still keep asking.
What it remembersOnly allowed-once. Ask once, ask again next time.
Logic trail · each animation step maps to a stretch of source
  1. Read this turn’s AskForApprovalprotocol.rs L924
  2. See whether FileSystemSandboxKind is Restrictedpermissions.rs L227
  3. Never gives Skip, UnlessTrusted gives NeedsApprovalsandboxing.rs L198
  4. OnRequest or Granular only ask when Restrictedsandboxing.rs L200
  5. Granular that must ask, with sandbox approval off, is Forbiddensandboxing.rs L209
  6. An execpolicy Prompt still clears a second gateexec_policy.rs L214
  7. Session cache only takes ApprovedForSession, keyed exactlysandboxing.rs L108
  8. When the policy changes, the model’s instructions change with itpermissions_instructions.rs L271
Hit Play to watch the same command go from a straight allow all the way to needing a human nod.
How many of the twelve cells matchread-only and workspace-write are both Restricted. OnRequest stamps the same ask on those two rows. danger-full-access twists kind to Unrestricted, and the default function stops asking.
Never can still denyWhen the policy requires asking, Never upgrades the ask into a deny. Turning off sandbox_approval only blocks a window that was going to pop. If the filesystem is already Unrestricted, the Forbidden branch cannot be entered.
Two kinds of rememberRemember npm run test for the session, then run npm run lint: the keys miss, so it asks again. Only remember-by-prefix widens the range. What a regular popup shows by default is the prefix option.
Teaching demo: the stage only demos the decision function and the two cache shapes. No real command is run. Line numbers on the logic trail match openai/codex commit 4f39251a01.
Idea 1 · Whether it asks by default is tied to the sandbox kind
What problem it solves

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

What the idea is

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

Command arrives AskForApproval Filesystem kind Restricted? Default need Skip / ask / deny Allow, then run Prompt clears one more gate OnRequest plus Unrestricted defaults to Skip. Never plus Prompt upgrades the ask into a deny.
Teaching diagram: emit the default need first, then let execpolicy stack one layer.

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

Why it lasts

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.

Idea 2 · After you asked, what does it remember
What problem it solves

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

What the idea is

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.

User clicks a decision ReviewDecision ApprovedForSession Exact key into the session drawer ApprovedExecpolicyAmendment Prefix written into the rule file Next time the full command matches only then skip the popup A prefix match allows Across sessions, wider range Approved allows this once and stays out of the map. npm run test and npm run lint are two keys. A regular exec default menu often only shows the prefix option.
Teaching comparison: one layer keys the full command; one layer keys a prefix and writes it to disk.
Why it lasts

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.

Idea 3 · When the policy changes, the instructions change with it
What problem it solves

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.

What the idea is

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.

When the policy changes, the instructions must change with it.
Why it lasts

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.

Side-by-side · Knobs independent, or tied together

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

Source checked on both sides · 2026-08-22 · DSH · Approvals & Permissions

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

Source checked on both sides · 2026-08-22
Classroom Exercise
01

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?

Takeaway:Whether the same command asks depends on the approval policy and whether the filesystem is Restricted. When Never meets a rule that must ask, the ask upgrades into a deny. Memory after you asked is two layers: the session layer keys exactly; only the durable layer allows a prefix. When the policy changes, that sentence to the model must change with it.