Reading contract. Follow a test command through policy, review, execution environment, and denial retry. Verified on 2026-09-20 against public source the fixed public source snapshot; private service behavior is outside scope.
Start with a normal action: the model wants to run tests. The shell tool call looks valid. Before anything runs, Codex still needs to know the current working directory, writable roots, network policy, approval mode, and whether any hook wants to block or rewrite the request.
That is the permission path this article follows. Permission is not a
single UI switch and not a prompt-only promise. It is a runtime path:
the turn carries policy, registry runs hooks, the orchestrator
decides approval and sandboxing, and the concrete runtime executes only
inside a selected SandboxAttempt.
In this article, side effects mean actions that touch the outside environment: starting processes, writing files, widening filesystem access, using the network, or retrying a denied sandboxed attempt with broader authority.
Source scope.
This article describes the policy fields, hook calls, approval events,
sandbox selection, and retry logic visible in the public openai/codex
source. Each decision maps to source objects such as
AskForApproval, PermissionProfile,
ExecApprovalRequirement, ToolOrchestrator, and
SandboxAttempt. It does not infer private guardian model
behavior or undisclosed operating-system sandbox details.
This part answers six questions:
- Which permission and sandbox fields enter at turn start?
- Where do those fields land inside the runtime?
- Where do pre/post hooks and permission-request hooks attach?
- How does
ToolOrchestratorchoose skip, reject, or approval? - How are the first sandbox attempt and retry attempt selected?
- How do we separate a model request from runtime authority?
1. Turn Start Establishes the Permission Inputs
Permission settings exist before any shell handler runs. App-server v2
TurnStartParams
includes approval_policy, sandbox_policy, and
permissions. The permissions field selects a named
permission profile and cannot be combined with sandboxPolicy.
TurnContext retains turn configuration, but execution reads its captured StepContext.settings for approval policy and the selected TurnEnvironment for permissions, workspace roots, and sandbox configuration. Queued calls therefore retain the settings of their originating step.
| Turn Field | Runtime Object | Meaning |
|---|---|---|
approval_policy |
AskForApproval |
When Codex should request approval or return failure. |
sandbox_policy |
SandboxPolicy |
Legacy sandbox shape: full access, read-only, workspace-write, and network flags. |
permissions |
PermissionProfile |
A richer profile projected into filesystem and network policies. |
runtime_workspace_roots |
workspace roots | The roots against which writable workspace policy is interpreted. |
Permission updates enter session configuration for later steps to capture. A permission profile and a legacy sandbox policy are two input shapes that ultimately produce filesystem and network policy. For a concrete command, follow tool.turn_environment(req) to identify the execution environment receiving those restrictions.
2. What Approval, Permissions, Sandbox, and Hooks Each Decide
Approval, permissions, sandboxing, and hooks all describe authority, but they answer different questions in the source.
| Term | Source Object | Question Answered |
|---|---|---|
| Approval policy | AskForApproval |
Should this action ask a user, guardian, or hook for a decision? |
| Permission profile | PermissionProfile |
Which filesystem, network, and additional permissions are active? |
| Exec requirement | ExecApprovalRequirement |
Is this request skipped, approved interactively, or forbidden? |
| Sandbox attempt | SandboxAttempt |
Which concrete execution environment is used for this attempt? |
| Hooks | pre/post/permission hooks | Where extension or policy code can intercept the flow. |
AskForApproval
defines modes such as OnRequest, Granular, and
Never. SandboxPolicy
describes execution restrictions such as danger-full-access,
read-only, external-sandbox, and
workspace-write. Approval decides whether to ask. Sandbox policy
decides the shape of containment.
Configuration compatibility is a separate question. The enum no longer has a distinct OnFailure variant, but OnRequest retains the serde alias on-failure. The old string still parses; that does not preserve an independent review-on-failure execution mode.
3. Registry Hooks Run Before the Handler
Before the handler executes, ToolRegistry gives pre-tool-use hooks
a chance to respond.
The dispatch path
obtains a pre-tool-use payload, runs run_pre_tool_use_hooks, and
either blocks the call, rewrites the invocation input, or continues.
After successful handler execution, post-tool-use hooks can add context or replace model-visible output. That logic is visible in the post hook block. These hooks wrap the handler. They are distinct from permission-request hooks, which sit inside approval.
Pre/post hooks intercept tool execution. Permission-request hooks intercept an approval request before it reaches guardian or user review.
4. Side-Effecting Tools Enter the Orchestrator
For commands, exec_command enters unified-exec management, which builds a UnifiedExecRequest and drives UnifiedExecRuntime through ToolOrchestrator. Patch execution supplies its own request and runtime to the same approval, sandbox-selection, and retry flow.
The module header of
orchestrator.rs
states the sequence: approval, sandbox selection, attempt, and retry
with escalation on denial. The generic shape comes from
Approvable, Sandboxable, and ToolRuntime.
Keep following one request: the model wants to run
npm test in the current workspace. Once it enters the
orchestrator, the record to track is closer to this shape:
{
"tool": "exec_command",
"input": {"cmd": "npm test", "cwd": "workspace"},
"approval_policy": "on-request",
"sandbox_attempt": {
"sandbox_requested": true,
"file_system": "workspace-write",
"permission_profile": "project-write",
"workspace_roots": ["workspace"]
},
"retry_reason": "sandbox_denied"
}
This record separates the decisions. Approval policy decides whether a review is needed. The sandbox attempt selects the first permission profile. The tool runtime turns that attempt into an executable environment. Only when failure looks like sandbox denial does the orchestrator consider escalation or retry.
5. First Decision: Does This Need Approval?
ToolOrchestrator::run reads permissions from the request’s environment, then asks for a custom exec_approval_requirement(req). Otherwise the defaults return Skip for Never, require review under restricted filesystems for OnRequest and Granular, and require it for UnlessTrusted. When sandbox review is otherwise required, Granular settings that prohibit it produce Forbidden; an unrestricted filesystem still defaults to Skip.
Skip does not unconditionally mean no review. With strict auto-review, the orchestrator still builds an ApprovalAction and ApprovalContext and calls Session::request_approval. Ordinary Skip continues, Forbidden rejects, and NeedsApproval uses that same central approval entry.
policy requirement: Skip / Forbidden / NeedsApproval
Skip + strict_auto_review -> request_approval(action, context)
Skip + ordinary mode -> select sandbox
Forbidden -> reject
NeedsApproval -> request_approval(action, context)
6. Permission Hooks Can Answer Before User Review
Session::request_approval centralizes approval. ApprovalAction represents commands, stdin writes, patches, MCP calls, network access, and permission requests, and converts each action to a permission_request_payload. PermissionRequest hooks run first: Allow approves, Deny rejects, and no verdict falls through to Guardian or user review.
Command actions produce a bash hook payload; patch actions use apply_patch. Retry hook ids carry :retry. This differs from registry PreToolUse: approval answers a specific authorization request, while PreToolUse checks or rewrites input before the handler. Approval still permits only the next attempt under the request environment’s restrictions.
7. After Approval, Select the First Sandbox Attempt
Once approval permits the attempt, the orchestrator resolves sandbox intent from permissions, overrides, and managed networking. Local execution uses SandboxManager::select_initial. When the executor manages process isolation, local SandboxType::None can coexist with sandbox_requested = true: the executor must enforce the requested restrictions.
SandboxAttempt carries sandbox intent, permissions, executor permissions, cwd, workspace roots, and network state. env_for and env_for_exec_server prepare the corresponding execution paths. Unified exec uses these fields to prepare commands; the patch runtime prepares filesystem operations.
Sandbox is not a cleanup step after failure. It is the environment for
each attempt. The runtime receives SandboxAttempt and executes
inside that attempt.
8. Retry After Denial Runs Through Policy Again
If the first attempt succeeds, the orchestrator returns output. The interesting path starts with sandbox denial. The retry branch checks network-denial context, whether the tool may escalate on failure, whether unsandboxed execution can preserve the active filesystem policy, and whether approval policy allows a retry prompt.
A retry carries a network- or sandbox-denial reason and may require fresh approval. Under strict_auto_review, the first approval covers only its sandboxed attempt and cannot bypass review of a broader retry. Denied-read restrictions prohibit removing filesystem isolation. Executor-managed isolation still uses local SandboxType::None when retry_sandbox_requested is true, with the executor receiving the restrictions.
after sandbox denial:
denial output + network policy
↓
tool escalation rules
↓
no-sandbox / network approval policy
↓
maybe request_approval(call_id:retry)
↓
retry attempt or denied output to the model
9. Approval Requests Are Structured Events
Clients receive structured requests. ExecApprovalRequestEvent includes command, cwd, reason, network context, additional permissions, and available decisions; ApplyPatchApprovalRequestEvent describes file changes and the grant root. The central approval module routes the action to a reviewer; the user route calls the session command or patch approval interface and returns a ReviewDecision.
A request sent to a reviewer, permission granted, and execution completed are three separate states. An approval event or Approved decision cannot replace process exit status and tool output. A retained process may require fresh approval for later stdin writes or network access.
10. Rules for Reading This Path
| Observation | Source Question | Source Entry Point |
|---|---|---|
| The model asks to run a command. | Did the handler translate it into a side-effecting runtime request? | unified_exec/apply_patch handler to ToolOrchestrator. |
| A tool asks for approval. | Is approval coming from default policy or a tool-specific requirement? | exec_approval_requirement and default_exec_approval_requirement. |
| No user prompt appears. | Did a permission-request hook already allow or deny it? | run_permission_request_hooks. |
| A command runs in sandbox. | Which sandbox, permissions, cwd, and network flags are on this attempt? | SandboxAttempt. |
| A denied sandboxed command retries. | Is it a network approval, no-sandbox retry, or terminal denial? | ToolOrchestrator::run retry branch. |
TurnContext retains turn configuration, but execution reads its captured StepContext.settings for approval policy and the selected TurnEnvironment for permissions, workspace roots, and sandbox configuration. Queued calls therefore retain the settings of their originating step.
The key distinction is that the model proposes an action. Runtime checks decide whether that action may touch the system. The next part follows these decisions into Windows users, ACLs, restricted tokens, and network rules enforced by the operating system.
Sources
- openai/codex · 5c5308fc9a9e
- TurnStartParams
- StepContext
- TurnEnvironment / permissions
- ApprovalAction / Session::request_approval
- hook / Guardian / user routing
- ToolOrchestrator
- default requirements / denied reads
- Approvable / SandboxAttempt
- UnifiedExecRequest / runtime
- ApplyPatch runtime
- PreToolUse / PostToolUse