Codex Under the Hood: Making the Sandbox a First-Class Agent Concern

Cover Image for Codex Under the Hood: Making the Sandbox a First-Class Agent Concern

Listen with Article TTS Reader

Checking for Article TTS Reader…

The previous post looked at a harness organized around recovery paths. Codex answers the same basic agent-loop problem with a different center of gravity: a model is about to touch a developer's files and execute commands, so containment and approval policy belong in the core runtime.

This article is based on the codex repository at revision 13fe2bcb7a. File references describe that revision only. They are implementation observations, not claims about later versions.

A turn ends only when several signals agree

The Rust core lives under codex-rs/, and core/src/session/turn.rs defines run_turn. Its own documentation describes the familiar behavior: request a model response, execute requested function calls, feed their output into another sampling request, and finish when the model supplies an assistant message without calls.

The actual termination decision is broader than that description. The inner sampling loop builds prompt input from a session-history snapshot, drains pending user input, and consumes streamed Responses API events through completion. A following sampling request can be required by any of these conditions:

  • a tool call was handled and needs its result returned to the model;
  • a malformed or rejected call generated a synthetic result that the model must see;
  • the service reports end_turn == Some(false);
  • pending user input needs to be incorporated.

The runtime combines those signals into model_needs_follow_up. A Stop hook can also veto completion and add a continuation message. Context overflow is handled inside the turn by compaction followed by another pass.

This is an important pattern for agent authors. A provider's end-of-turn flag is one input to a local state decision. It cannot be the only one when local tool execution and interactive input can still create unfinished work.

One execution surface for long-running shell work

Codex exposes shell work through exec_command and write_stdin in core/src/tools/handlers/shell_spec.rs. The pair sits on top of PTY sessions, allowing a process to continue running while the agent sends more input and retrieves output in chunks.

The execution schema carries permission intent with each call:

sandbox_permissions: use_default | with_additional_permissions | require_escalated
justification
prefix_rule

That makes an elevation request visible in the model's tool invocation rather than an invisible side channel. The router converts response items into a common ToolCall, then ToolCallRuntime uses Tokio to run compatible calls concurrently. Its locking model uses read locks for parallel work and a write lock for serial work.

Recoverable call errors return a success: false result to the model. Only a fatal error stops the turn. The model can therefore respond to a failed command with a different command or a request for approval, which is usually more useful than tearing down the entire interaction.

Command safety comes from three independent policies

The most distinctive design is a three-layer decision about whether a command can run.

SandboxPolicy controls what a process can reach

SandboxPolicy in protocol/src/protocol.rs describes access modes from read-only through workspace-write to danger-full-access, with network controls alongside them. A writable root can include read-only subpaths. The reviewed source uses that capability to protect .git/hooks and .codex, reducing the chance that an agent could rewrite its own control surfaces as part of an escalation attempt.

AskForApproval controls when a person decides

AskForApproval supplies the interaction policy: untrusted, on-request, granular, or never. This layer answers a different question from the sandbox. A command may be technically constrained and still need an explicit human decision; another may execute within a limited boundary without one.

execpolicy evaluates the command itself

The execpolicy rules engine evaluates parsed commands and returns Allow, Prompt, or Forbidden. In the reviewed code, bypassing sandbox execution requires every command segment to match an explicit Allow rule. Approved prefix_rule amendments update the active policy and are persisted as default rules, so their effect can survive the immediate session.

Separating these policies avoids an overloaded approval switch. Process reach, human confirmation, and command-specific rules can change independently.

The platform implementations also need careful wording. At this revision, macOS generates a Seatbelt profile for sandbox-exec; Linux uses bubblewrap for the default filesystem-isolation route with no_new_privs and seccomp in the process; Windows has a restricted-token, ACL, and private-desktop implementation whose effective policy can resolve to disabled without the relevant configuration or feature. Network controls can live in the sandbox profile or use a local inspection proxy. These are implementation details from the cited revision, not a ranking of the current safety posture of different tools.

JSONL remains the canonical replay record

The message model mirrors the Responses API through ResponseItem variants including messages, reasoning, function calls, and compaction records. Conversations are persisted as JSONL rollout files whose lines carry a timestamp, ordinal, and item. SQLite holds queryable thread metadata, while the JSONL rollout stays the canonical history.

The history API makes the lifecycle explicit with InitialHistory::{New, Cleared, Resumed(..), Forked(..)}. Resume replays a rollout. Fork can copy or reference the original persistence. A subagent is implemented as a fork of its parent's persisted history, which is an economical representation: delegation inherits a specific historical state rather than needing an unrelated state machine.

Compaction also accounts for model changes. In turn.rs, a change in compatible model hashes can trigger compaction using the preceding model's context. Moving to a smaller context window triggers downshift compaction only when the active context exceeds the new limit. The source's COMPACT_USER_MESSAGE_MAX_TOKENS limits retained user messages after compaction, rather than the summary output itself. These distinctions matter when debugging why a resumed thread became shorter.

The runtime is intended to be an engine as well as a CLI

codex app-server exposes a JSONL, JSON-RPC-like protocol over standard input and output, with v1 and v2 protocol shapes. The reviewed README explicitly says that the experimental WebSocket transport is unsupported and that wire payloads do not include the conventional "jsonrpc":"2.0" field.

The repository also contains both directions of MCP support: it can consume external MCP servers and expose Codex as an MCP server. A cached, sticky WebSocket model transport and experimental code mode point in the same direction. The terminal user interface is one client of an underlying agent runtime that other clients can integrate with.

What this revision shows

Codex places execution safety at the center of its harness design. The turn loop, tool schema, sandbox policy, approval policy, and command rules all contribute to the decision to run a command. Its replayable history and app-server interface make the runtime useful beyond one terminal session.

That integration has a cost. The reviewed workspace has roughly one hundred Rust crates, and several transport and API choices are closely coupled to the OpenAI ecosystem. Those tradeoffs are visible in the repository revision; they are not a judgment about every possible deployment.

The next post examines opencode, which takes a TypeScript and durable-state-projection route through many of the same problems.

Source index

  • Turn loop: codex-rs/core/src/session/turn.rs:141-155,303
  • Shell tool schema: codex-rs/core/src/tools/handlers/shell_spec.rs:91-155,228-274
  • Parallel tool runtime: codex-rs/core/src/tools/parallel.rs:145-241
  • Sandbox and approval policies: codex-rs/protocol/src/protocol.rs:962-1114
  • Command-rule decisions: codex-rs/execpolicy/src/decision.rs:9-16
  • Rollout recorder: codex-rs/rollout/src/recorder.rs:86
  • History lifecycle and subagent fork: codex-rs/history/src/lib.rs:222-227; codex-rs/core/src/thread_manager.rs:988
  • App-server protocol: codex-rs/app-server/README.md:20-37