Approval Policy: Same Command, Two Knobs Decide Whether to Ask
Pair the same command with different approval policies and the ending swings from a straight allow to a human nod. Whether it asks by default depends on whether the filesystem is Restricted. The session cache keys on an exact command.
THE QUESTION THIS PAGE ANSWERS
ANSWER FIRSTWhat is the key idea behind “Approval Policy: Same Command, Two Knobs Decide Whether to Ask”?
Pair the same command with different approval policies and the ending swings from a straight allow to a human nod. Whether it asks by default depends on whether the filesystem is Restricted. The session cache keys on an exact command.
Follow the handoffs, not the demo. A system becomes dependable at the boundaries between model, tools, state, permissions, and people. Read each handoff as a place where you can observe, test, and recover.
Name the input, owner, approval, and recovery action for one automated step.
A successful run that cannot explain what happened or be safely repeated.
- 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?
The handoffs inside “Try it first · Same command, swap the policy, watch the ending”
“You leave approval on on-request and twist the sandbox from workspace-write to danger-full-access .” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.
Write the state before adding capability
Starting from “Approval policy AskForApproval is which ask-a-human rule this turn uses;”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.
- Read this turn’s AskForApproval protocol.rs L924
- See whether FileSystemSandboxKind is Restricted permissions.rs L227
- Never gives Skip, UnlessTrusted gives NeedsApproval sandboxing.rs L198
A happy path is not reliability
Use “Then approve the same npm run test for the session, and submit npm run lint .” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.
From “Try it first · Same command, swap the policy, watch the ending” to “Idea 1 · Whether it asks by default is tied to the sandbox kind”
“Try it first · Same command, swap the policy, watch the ending” grounds the problem in “Four policies, three sandboxes: which cell stamps allow, ask, or deny Play Step Reset Cmd git status npm run test npm run lint prompt rule The first three take Allow. The fourth hits execpolicy Prompt. Policy O…”. “Idea 1 · Whether it asks by default is tied to the sandbox kind” then moves it toward “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 approv…”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.
Carry the judgment into the next situation
When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.
- “Try it first · Same command, swap the policy, watch the ending”: Four policies, three sandboxes: which cell stamps allow, ask, or deny Play Step Reset Cmd git status npm run test npm run lint prompt rule The first three take Allow. The fourth hits execpolicy Prompt. Policy O…
- “Idea 1 · Whether it asks by default is tied to the sandbox kind”: 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 approv…
- “The closing point”: Granular that must ask, with sandbox approval off, is Forbidden sandboxing.rs L209
The final “The closing point” brings the discussion to “Granular that must ask, with sandbox approval off, is Forbidden sandboxing.rs L209”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.
I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.
After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.
When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.
No discussion on this article yet.