# Codex from the source: the harness, crate by crate, and what is and is not open

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/codex-harness-from-the-source
> date: 2026-10-31
> tags: agents, security, agentic-coding, developer-tools, mcp

A post that went around on 30 October says OpenAI open-sourced the whole Codex harness: the CLI, the Skills and the runtime environment, "for anyone who wants to learn how agents are built". Codex is not new. The `openai/codex` repository was created on 2025-04-13, it is Apache-2.0, and the pieces the post names were all in the history I could read from its first day. What the post gets right is that the harness really is all there, and that makes it the most complete public specimen of a production coding-agent harness I know of.

So I read it. This is a teardown of `openai/codex` at commit `67f0994b` (2026-10-31, "Tighten Bazel CI snapshot handling and OTLP test cleanup", #58459): 117 top-level crates, about 2.2 million lines of Rust, and a pace of 45 to 79 commits on a normal weekday. I did not build the whole product, but in a second pass I built and ran its small, self-contained crates (the patch applier, the rule engine and the shell-command parser) against my toys, and what that showed is in its own section below, "What running it showed". Everything else is read from source, counted by script, or computed by four small programs of mine that re-implement pieces of the logic. Where a number came from a research pass and not from me, I re-checked it: I rebuilt the crate graph independently from the `Cargo.toml` files and got the same 584 edges, and I recounted the feature flags and the protocol, which corrected two of my own first-pass numbers (the flags are 201, not 195).

## What the post gets right, and what it does not say

<VerdictBoard />

**In the repo (read).** The agent loop and tools, every sandbox (Linux, macOS, Windows), the terminal UI, the CLI and the non-interactive `exec` mode, the app-server and its protocol, the MCP client, the skills loader, plugins, hooks, the V8 code-mode runtime, the Python and TypeScript SDKs, and 31 CI workflows. `README.md` calls it "a coding agent from OpenAI that runs locally on your computer".

**Not in the repo (read, and inferred where marked).** The model weights and the Responses API server. The hosted Codex cloud service: the repository has clients for it (`cloud-client`, `cloud-tasks-client`, `backend-client`) and not the service. The fleet that runs executors: `exec-server` is the executor process, not the machines. ChatGPT account and billing backends: `login`, `workload-identity` and `user-verification` are client-side. The catalogue of Apps connectors: the plumbing (`connectors`, `codex_apps.rs`) is open and which apps exist is not. And pull requests: GitHub reports `pull_request_creation_policy: collaborators_only`, so the code is open and external contributions mostly are not.

**Not new (measured).** In the 6,300-commit window I could read (2026-05-19 to 2026-10-31), fourteen of the paths the post names, among them `skills`, `code-mode`, `app-server`, `hooks`, `linux-sandbox`, `windows-sandbox-rs`, `exec-server` and both SDKs, are present on the first day. There is no single open-sourcing commit. Some pieces are young: `ext/skills` appears on 3 June, `code-mode-runtime` on 30 July, `mxc-sandbox` on 4 September, `ext/guardian-reviewer` on 9 September, `message-board` on 21 October and `plugin-host` on 26 October. What I cannot see is the first thirteen months of history, so "public since creation" is an inference from the creation date and the star count, not something I observed.

**One more thing the commit trailers say (inferred).** 4,857 of the 6,300 commits I can read carry a `GitOrigin-RevId:` trailer. That is the usual mark of a repository exported from an internal source tree, so the open repository looks like a mirror of a private one that moves in bursts of dozens of commits a day. That fits the collaborators-only pull request policy, and it is a reading of the trailers, not a statement from the project.

A word on the labels. **Read** means I read it in source at the cited path and line. **Measured** means I counted it with a script. **Inferred** is my reading, not something the code states. **Reported** is a claim I did not check. A "toy" is a re-implementation I wrote to see behaviour; it is not the Rust code and it does not claim to be.

## The repository in numbers

Counted by script over the whole checkout (measured): 5,719 Rust files and 2,222,503 lines of Rust, 22,217 test attributes, 169 Python files with 58,054 lines (the SDK, generated types and tooling), and only 3,413 lines of TypeScript, all of it the TypeScript SDK; the terminal UI is Rust. The Cargo workspace lists 166 members, among them 29 `utils/*` sub-crates and 18 `ext/*` extension crates; 117 directories hold a top-level crate. There are 31 GitHub workflows, 32 CLI subcommands, 103 top-level configuration keys and 201 feature flags.

The test code is as large as the product code: 1,064,390 non-test and 1,045,973 test lines in the top-level crates, by the naming rule I used. In `core` alone the ratio is 2.65 test lines to every source line. The terminal UI is as big as the engine, 478,950 lines against 488,661. The front end is not a thin shell.

<CrateMap />

I grouped the 117 crates into six layers (the grouping is mine; the counts are measured). Bottom to top: protocol and schema (13 crates, 91k lines), services and I/O (24, 114k), execution and sandbox (19, 211k), the core engine (30, 703k), extensibility (18 crates plus the 18 `ext/*` ones, 198k plus 83k) and hosts and front ends (13, 788k). Click a box for any crate's size, tests and fan-in.

<DependencyHubs />

The dependency graph has a clear shape. `codex-protocol` is the shared vocabulary: 59 other crates depend on it. `codex-http-client` is next with 39, then `codex-config` with 22. At the other end `codex-core` depends on 52 workspace crates, the app-server on 46, the CLI on 41 and the TUI on 38. There is also a `core-api` crate with 14 dependencies, which I read as the narrower surface hosts can depend on instead of `core` itself (inferred).

<RepoTempo />

The repository moves fast. The 6,300 commits break down as 352 in the second half of May, 929 in June, 879 in July, 1,266 in August, 1,617 in September and 1,257 in October. The last 300 commits touch `core` 142 times, the TUI 71 times and the app-server 62 times, and 67 distinct authors committed in that stretch. The median commit touches 7 files. Three of the last week's themes are visible in the commit messages: Guardian context budgeting (#58388, #58444, #58454), host-facing hooks and metadata (#58395, #58390, #58406), and plugin and app settings (#58428, #58441).

## One turn, as the code runs it

The session is driven by a submission loop that receives `Op` values: 32 variants, from `Interrupt` and `TurnInput` to `ExecApproval`, `Compact` and eight realtime-conversation operations. Events come back as `EventMsg`, with 86 variants. A turn runs as a `RegularTask`, and `run_turn` (`core/src/session/turn.rs:177`, in a file of 3,612 lines) is the heart of it. Its own doc comment states the loop: the model replies with function calls or a message; a call is executed and its output goes into the next request; a message alone ends the turn.

<TurnLoop />

Read the diagram as two loops stacked. The inner loop is one model request. The outer loop is `needs_follow_up = model_needs_follow_up || has_pending_input`: if the model made tool calls, or the user typed something while it was working, there is another request. Between them, a compaction decision: `should_roll_over` is true when auto-compaction is on, a follow-up is needed, and either a new context window was requested, the token limit was reached or an async candidate is ready (`turn.rs:636-696`).

Two details are worth stopping on. First, a step captures one frozen view of context, advertised tools and tool calls ("capture once so context, advertised tools, and tool calls share one request view", the comment at `turn.rs:501`), so a change in the middle of a step cannot make the request inconsistent with itself. Second, steering: input the user types while the model runs is queued and drained at the top of the loop, which is how you can talk to the agent mid-turn.

### Tools start while the model is still talking

The stream handler is where the design gets interesting. When the model finishes emitting an item and that item is a tool call, `handle_output_item_done` records it in history at once and dispatches the handler: a comment says the call "dispatches immediately, not when its future is polled". The future goes into a `FuturesOrdered`. The stream keeps going. When it ends, `drain_in_flight` awaits the futures in call order and records each output.

<SamplingTimeline />

That has two consequences, both from the code and neither stated outright. A model that emits a tool call early in a long response gets that tool running in parallel with the rest of its own output (inferred from the control flow). And outputs enter history in the order the calls were made, not the order they finished: a fast second tool waits behind a slow first one before it is recorded. Flip the toggle in the timeline to see it.

### The gate: many readers, one writer

How many tools run at once is decided by a single lock. Each turn owns one `tokio::sync::RwLock<()>` (`core/src/tools/parallel.rs:54, 92`). A call whose runtime supports parallel calls takes `lock.read()`; every other call takes `lock.write()` (`parallel.rs:284-300`). Request-level parallelism is hard-coded on: `parallel_tool_calls: true` goes in every prompt. Whether a tool is parallel-capable is a property of its runtime, and `supports_parallel_tool_calls` is true for all twelve catalog models.

A lock is a scheduling policy, so I simulated it. This is toy 2: a discrete-event model of a fair first-in-first-out readers-writer lock (tokio documents its lock as fair; I did not re-test that), eight calls of one second each. All reads: 1 second against 8 serial. Seven reads and a write: 2 seconds. One write in the middle: 3 seconds, because the four readers queued behind the writer wait for it even though they could have run beside the first three. Alternating reads and writes: 8 seconds, no better than serial. Over 400 random placements of exclusive calls among eight, one exclusive call already cuts the speed-up from 8x to 2.9x (a mean of 2.75 seconds), two give 1.9x, and with four of the eight exclusive the gate is only 1.24x faster than a plain queue.

<LockGate />

The durations and arrival gaps are my assumptions; the shape is the rule's. It says something practical: the gain from parallel tool calls depends on how the model orders reads and writes in a response, and a writer early or in the middle limits it more than one at the end.

## What the model is sent

`build_prompt` (`turn.rs:1751-1779`) returns a `Prompt` with a handful of fields: the `input` items, the `tools`, `parallel_tool_calls` (hard-coded true), `base_instructions`, an optional output schema and a flag for the "cyber access program". Read in order, a request is the system prompt, the tool list, and then the input: developer-role messages (permissions, collaboration mode, instructions), contextual user-role messages (the environment, `AGENTS.md`), and the conversation with its tool calls and outputs.

### The prompt lives in the model catalog

The system prompt is not a file in the loop. It ships in the model catalog, `models-manager/models.json` (539,696 bytes, 12 models), as a per-model `instructions_template`. The twelve templates run from 2,735 to 3,543 words (measured), and `base_instructions` is empty for all of them. The catalog slugs, such as `gpt-6-astra` or `gpt-5.5`, are what the file says; I cannot tell which are public names.

The first template opens "You are Codex, an agent based on GPT-6. You and the user share one workspace" and goes on through headings I scanned: "When to ask the user for permission", "Autonomy and persistence", "Personality" (with writing-style and technical-communication parts), "Working with the user" (intermediate commentary, final answer, formatting rules), "Rules for getting work done", "Using skills", "Apps (Connectors)" and "Plugins". Around the template, each model entry carries message sets for persistent mode, the four approval modes (`on_request`, `on_request_auto_review`, `never`, `unless_trusted`), collaboration modes (`default` and `plan`), auto-review policy text, multi-agent roles, a token budget and confirmation policies for browser and computer use.

Older prompts stay in the tree for older model slugs: `core/gpt_5_codex_prompt.md` is 1,082 words, and the 5.2 file adds a front-end section. Two behavioural rules from the older prompt are worth knowing because they shape what an agent will do to your repository: "NEVER revert existing changes you did not make unless explicitly requested", and do not amend a commit unless asked. Plan mode is its own 1,369-word template, three phases, and "You are in **Plan Mode** until a developer message explicitly ends it."

### Context is a structure that gets diffed

The first context of a session is built from developer and contextual sections. After that, each step records only a diff. The unit is the `WorldStateSection` trait (`core/src/context/world_state/mod.rs:320`): every section has a stable `ID`, a serializable snapshot and a `render_diff(previous)` that returns the new snapshot plus whatever message describes the change. The previous state is `Absent`, `Unknown` or `Known`, and a SHA-1 over canonical JSON fingerprints what the model has seen (`mod.rs:370`). There are 18 section ids, from `agents_md` and `permissions` to `plugins_instructions` and `top_level_tools` (measured by grep), and 52 fragment types under `core/src/context/`.

<ContextDiff />

Which section a given change re-renders is my inference from the names; the mechanism is read. Its effect is the point: when the working directory, the sandbox permissions, the mode, the model or the tool list changes mid-session, the harness appends a small message and leaves the early part of the request byte-identical, which is what a provider's prompt cache needs. Rollback is aware of it too: `trim_pre_turn_context_updates` strips the contextual items immediately above a rolled-back turn and, if a mixed bundle was trimmed, clears the baseline so the next turn re-injects full context.

For eleven of the twelve catalog models there is a further step, "incremental tools" (`use_responses_lite` is true for all but `gpt-5.5`). Once tool declarations are in history, `current_window_uses_incremental_tools` returns true and the request goes out with an empty `tools` array and an empty `base_instructions`; later changes arrive as messages, with default wording "This is an incremental tools update. Previously declared tools remain available for direct calls unless explicitly marked unavailable." The flag was added on 2026-10-02 (#50464) and is stable and on by default. The client also keeps a WebSocket per turn and chains requests with `previous_response_id`, and prewarms with a `response.create` that has `generate=false` (`core/src/client.rs:1-24`).

This connects to an earlier piece on this site, [prompt caching in coding agents](/articles/prompt-caching-harnesses), which read Codex at an older commit (`ce0323f`, 17 October) and found a session-scoped cache key, the base instructions first, and changes appended at the tail. What I describe here is the same instinct taken further: for models that use incremental tools, the head of the request is nearly empty and constant. I did not measure cache hit rates, and the article's own caution applies: a stable prefix is a design, not a bill.

History is normalized before every request: every tool call gets an output, outputs without a call are removed, and images or audio are stripped for a model that cannot take them. Token estimation is crude and honest: four bytes per token, rounded up (`utils/string/src/truncate.rs:6`), with fixed estimates for an audio file reference (12,000 tokens) and a resized image. Tool output is cut to 10,000 tokens for every catalog model, in the middle, behind a "Warning: truncated output (original token count: N)" line.

### Where compaction fires

All of the thresholds are arithmetic on the catalog entry, so I computed them for every model in it. This is toy 3, run on the repo's own `models.json` with the formulas at `protocol/src/openai_models.rs:552-575` and `core/src/session/context_window.rs:35-110`. The resolved window is `context_window` or, if absent, `max_context_window`. The auto-compact limit is nine tenths of it, because the catalog's own `auto_compact_token_limit` is null for all twelve (with a limit set, the code takes the smaller of the two). Two details the first version left out: `model_auto_compact_token_limit_scope` can count only the growth after the carried prefix (`body_after_prefix`; the default is `total`), and when a token budget is on, a fallback buffer is added to the limit before the check. The hard limit is the window times `effective_context_window_percent`, which is 95. For a 272,000-token model that is 244,800 and 258,400; the 500,000-token entry gives 450,000 and 475,000, and the 372,000-token one 334,800 and 353,400. The gap between "compact soon" and "stop" is 13,600 tokens on the common model.

<WindowBar />

Compaction has more than one trigger (read): before sampling (`run_pre_sampling_compact`), in the middle of a turn when a follow-up is needed and the limit is reached, after a turn if `model_post_turn_compact_threshold_percent` is above zero (its default is 0, so off), and when a Guardian reviewer hits `ContextWindowExceeded`. If you switch models and the new one has a different compaction hash, or a smaller window than the old one needs, the previous model summarizes the old window and the new one owns the result. An async variant exists behind a feature (`AsyncCompaction`, under development, off) with a 65 percent default threshold whose base I could not pin down, and a "token budget" feature (off for the first eight catalog models) with a 6,144-token reminder and a 16,384-token fallback buffer.

<CompactionShape />

The local summary is produced by asking the model to act as a "CONTEXT CHECKPOINT COMPACTION" and write a handoff for another model that will resume the task. The new history is the initial context again, the newest user messages that fit in 20,000 tokens (newest first, an oversize one truncated and flagged), and one summary whose prefix tells the next model that another language model produced it. Remote compaction v2 keeps up to 64,000 tokens of retained messages, at most 10,000 per agent message, and allows two stream retries.

## Tools, and the one that edits your files

The fixed core of the tool set is about twenty names, plus eleven for multi-agent work, plus whatever MCP servers and plugins add (my count: 39 name strings matched, of which 19 were test fixtures or examples). Tools are declared in `core/src/tools/spec_plan.rs`, handled in `core/src/tools/handlers/` (57 files), routed in `router.rs` and registered in `registry.rs`.

| Tool | What it is | Notes (read) |
|---|---|---|
| `exec_command`, `write_stdin` | Run a command in a PTY and feed it input | `yield_time_ms` defaults to 10,000 (250 to 30,000 on Unix); `max_output_tokens` 10,000; a session id lets a long command be polled |
| `apply_patch` | Edit files | A freeform tool with a Lark grammar, "so do not wrap the patch in JSON" |
| `update_plan` | A planning checklist | The prompt says to skip it for the easiest quarter of tasks |
| `view_image`, `web_search` | Look at an image; hosted search | Search is hosted by the API, not run locally |
| `request_user_input`, `request_permissions` | Ask the user a question; ask for wider access | Granted permissions apply to later commands this turn or the session |
| MCP: `list_mcp_resources`, `read_mcp_resource`, `tool_search` | Resources and discovery of deferred tools | Namespaced `mcp__server__tool` |
| `get_context_remaining`, `new_context` | The model asks how much window is left, or asks for a fresh one | The agent can drive its own compaction |
| `exec`, `wait` | Code mode | In code-mode-only turns these replace the direct tools |
| `spawn_agent`, `send_input`, `wait_agent` and others | Multi-agent | Two generations of names (v1 and v2) coexist |

### apply_patch is a tool with a matcher

Edits go through a patch format with a small grammar: a begin line, then any number of add, delete and update hunks, an optional move, `@@` context lines, lines prefixed with a space, a plus or a minus, an optional end-of-file marker, and an end line. A lenient parser also accepts a patch wrapped in a shell heredoc, because some models emit it that way. Before the user is asked to approve, `maybe_parse_apply_patch_verified` derives every update hunk against the current file contents so an approval prompt can show a unified diff; a patch passed as the whole shell command ("implicit invocation") is rejected, and an `apply_patch ...` typed into `exec_command` is intercepted and routed to the patch engine instead of a shell.

The part that decides what a patch does to your file is `seek_sequence` (`apply-patch/src/seek_sequence.rs:12-104`). It locates a hunk's old lines in four passes of falling strictness: exact, then ignoring trailing whitespace, then trimming both ends, then trimming and normalizing Unicode punctuation (dashes, curly quotes and odd spaces to ASCII). Each pass scans the whole remaining file before the next pass starts. An end-of-file hunk first tries the end of the file. If the old lines are not found and the pattern ends in an empty string, it retries without it. Context lines inside a hunk stay as they were in the file, so mixed line endings survive.

<PatchLadder />

The matcher above is a live port; edit the pattern and watch which pass it lands on. To see what that does to real patches I wrote toy 1: a Python port of the matcher, the chunk parser and the apply loop, run over eighteen patches I constructed to probe it. They are not model output, so they say what can happen and not how often. The tally: 12 applied, of which 4 were not as intended; 2 partial; 4 clean errors. I later fed the same patches to the real `apply_patch` (measured; see "What running it showed"): the final file contents matched the port for 17 of 18, with the same four wrong-place edits, and the eighteenth could not be run here.

The four that landed wrong all follow from one property, that the passes are file-global and the replacement text is the patch's own. With no context line, a hunk that removes `return None` matches the first such line in the file, not the one you meant. A hunk written without the file's indentation matches on the third pass, and the replacement lines are written as the patch spells them, so a Python block comes out misindented. A hunk meant for a line with a trailing space loses to an exact match in another function, because pass one finds the exact line anywhere before pass two looks near the right place. And two lines that are the same once trimmed, at different nesting depths, resolve to the first. A `@@` context line fixes the first case, which is what it is for.

The failure modes are more careful than the matcher. A match failure in any hunk of a multi-file patch is caught in the verify step, before any write: in the toy, nothing changed. Run the apply loop alone, without verification, and the same failure leaves the earlier file modified. A write failure after the verify step, such as a full disk, leaves earlier files written; the code says so in a comment, "a failed write can still have modified the target before surfacing an error", and marks the recorded delta as no longer exact. A patch that touches the same path twice is refused by the verify step. An earlier version of this piece put that refusal in the apply loop, which was wrong: the real standalone `apply_patch` binary applies both hunks to the same file, and only the harness's pre-approval step rejects it ("multiple operations target …"). In short: matching is all-or-nothing in the harness path, writing is not transactional (`apply-patch/src/lib.rs:438`).

## When a request fails

Streaming errors go through a retry ladder (`core/src/responses_retry.rs`, `async-utils/src/backoff.rs`). Backoff starts at 200 milliseconds, doubles on each attempt and carries plus or minus ten percent jitter (the code draws a factor from 0.9 to 1.1), so the five default stream retries wait about 0.2, 0.4, 0.8, 1.6 and 3.2 seconds. 29 error kinds never retry at all, among them a full context window, a usage limit, an invalid request and the policy errors; those return to the turn loop, which decides what to do. If the retries run out, Codex tries once to switch from WebSocket to HTTPS and resets the counter. One case never gives up: with the default-on `UnboundedConnectionRetries` feature, a connection failure on a sampling request retries forever, 5 seconds doubling to a 60-second cap, under a "Reconnecting... waiting for network" message.

<RetryLadder />

## The sandbox

Codex runs commands inside a sandbox it builds itself, per platform, from one permission model. The legacy way to say it is three sandbox modes: `read-only` (the default), `workspace-write` and `danger-full-access`. The type the runtime actually uses is a `PermissionProfile`: `Managed` (Codex builds the sandbox), `Disabled` (no outer sandbox) or `External` (an outer caller enforces filesystem isolation). The built-in profiles are `:read-only`, `:workspace` and `:danger-full-access`, and a user can define `[permissions.<id>]` profiles that extend another. Network is two-valued, `Restricted` or `Enabled`.

Filesystem access is `read < write < deny`, and the rule for conflicts is in a doc comment at `protocol/src/permissions.rs:120`: the most specific entry wins, and when two equally specific entries target the same path, deny beats write and write beats read. Paths can be literal or special (`Root`, `Minimal`, `ProjectRoots`, `Tmpdir`, `SlashTmp`), and an unknown token is kept and ignored with a warning so that a config written by a newer Codex still loads in an older one. On top of every writable root sits a short list of protected names, `.git`, `.agents`, `.codex` and `.aws` (`permissions.rs:40-50`): they stay read-only inside a root that is otherwise writable. The backend is chosen per platform: bubblewrap with seccomp on Linux, `sandbox-exec` on macOS, a restricted token or a container on Windows (`sandboxing/src/manager.rs:50`).

<PolicyLattice />

### Linux: bubblewrap, then seccomp, in two stages

The Linux backend is `linux-sandbox` (39 files, 12,852 lines), which wraps a vendored copy of bubblewrap. Its README is specific about the supported environments: the helper uses the first `bwrap` on `PATH` outside the working directory and otherwise the bundled one, WSL2 takes the normal path and WSL1 is rejected, and the older Landlock path is refused for filesystem-restricted policies "because it cannot isolate app-server Unix sockets". The vendored bubblewrap is 0.11.2, released 2026-04-23, whose news file records a fix for CVE-2026-41163 in setuid mode.

What it builds is a command line, and the order in which it builds it is the policy. This is the argv for a workspace profile, in the order `create_bwrap_flags` and `create_filesystem_args` emit it.

<CodexArgvExplorer />

Three decisions in it are worth reading closely. First, the protected names are not skipped when they are missing: a `.git`, `.agents`, `.codex` or `.aws` that does not exist yet is created as an empty read-only mount target (`--perms 555 --tmpfs <path> --remount-ro`), so the sandboxed command cannot create it, and other missing read-only paths get an empty file bound over them. Second, deny-read directories become an empty tmpfs with mode 000, or 111 if a writable child has to stay reachable inside them, and unreadable globs are expanded before launch by running ripgrep, with a fallback walker; the README says any other ripgrep failure aborts sandbox construction. Third, the code fails closed on a symlink: if a read-only or deny path crosses a writable symlink, the sandbox refuses to start, because masking the current target would be a snapshot that can go stale (`bwrap.rs:700-715`, `1349-1364`). Eighteen distinct bubblewrap flags appear in the file (measured).

The helper does not apply the filter before the box exists. It re-executes itself in two stages. The outer stage resolves the permission profile, prepares the host side of the proxy bridge when there is one, builds the argv and runs bubblewrap. The inner stage runs inside the new namespaces and only then does the locking-in: it verifies the descriptor-backed mounts, reads its own capability sets and panics if any effective or permitted capability remains, brings up the proxy routes, sets `no_new_privs` and installs the seccomp filter, then forks and execs the command while the parent forwards signals and reaps.

<HelperTimeline />

A note on `no_new_privs`: the code sets it only when it is needed, because many bubblewrap deployments rely on a setuid binary. Another on `/proc`: by default the helper mounts a fresh one, and retries without if that is denied, in which case the README says process ids inside can differ from those in `/proc`. A startup-only inherit mode skips the new PID namespace and lets sandboxed commands signal other same-user processes, including the executor; the README states this, and it cannot be set from repository config.

### What the seccomp filter says, and what it leaves out

The filter has three modes, picked by the network policy: `Restricted`, `ProxyRouted` and `VmSocketRestricted`. The default action is allow, a matching rule returns `EPERM`, and only x86_64 and aarch64 are supported. In every mode except the last, `ptrace`, `process_vm_readv` and `process_vm_writev` are denied. In all three, `io_uring_setup`, `io_uring_enter` and `io_uring_register` are denied, with a comment that `io_uring` can create `AF_VSOCK` sockets without a `socket()` call. In `Restricted` mode there are 13 more denials: `connect`, `accept`, `accept4`, `bind`, `listen`, `getpeername`, `getsockname`, `shutdown`, `sendto`, `sendmmsg`, `recvmmsg`, `getsockopt` and `setsockopt`, with `recvfrom` deliberately left allowed so that tools such as `cargo clippy` can use socketpairs, and `socket` and `socketpair` are denied unless the family is `AF_UNIX`. That is 19 syscalls denied outright, plus the two conditional ones.

<SeccompMatrix />

The proxy-routed mode permits IP sockets so a command can reach the local bridge; standalone Unix sockets need an explicit managed-policy grant. The VM-socket mode denies only `AF_VSOCK`, because VM sockets reach host services outside the filesystem sandbox, and on WSL2 they can launch Windows processes through the interop socket.

What the filter does not contain is as informative. I grepped the Rust sources outside `vendor/` for `SYS_unshare`, `SYS_mount`, `SYS_setns`, `CLONE_NEWUSER` and `disable-userns` and found none: there is no rule for `unshare`, for namespace flags to `clone` or `clone3`, for `mount`, `umount2`, `pivot_root`, `setns` or the new mount API, and Codex never passes bubblewrap's `--disable-userns`. That is a measured absence. What it means is a different question, and I come back to it below.

### The network path

With a managed proxy, the command lives in a namespace with no network and a bridge: TCP to a Unix domain socket to TCP. The host side listens on a Unix socket; inside, the helper brings up loopback, binds a local port and rewrites sixteen proxy environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, the `YARN_*`, `NPM_CONFIG_*`, `BUNDLE_*` and `DOCKER_*` families, and others) to point at it. The bridge writes an attribution frame with a per-command token, so the proxy can tie each connection to the command that made it, which is how an approved `curl *` can auto-allow its own requests.

The proxy itself is a 35,081-line crate with an HTTP listener on `127.0.0.1:3128` and SOCKS5 on `8081`. From its README (read): it is allowlist-first, so with no allow entries everything is blocked; hosts can be exact, `*.x` for subdomains or `**.x` for the apex and subdomains; matching is ASCII-case-insensitive with no IDNA conversion; and deny wins. A `limited` mode allows only `GET`, `HEAD` and `OPTIONS` and needs TLS interception to enforce the method over HTTPS. A host that resolves to a loopback, private or link-local address stays blocked even if allowlisted. The proxy can terminate TLS with an in-memory CA, apply hooks that strip headers, re-sign AWS requests so the workload only ever holds dummy credentials, and emit an audit event per decision. Its README is candid about a limit: "DNS rebinding is hard to fully prevent without pinning the resolved IP(s) all the way down to the transport layer", and it advises enforcing egress at a lower layer too.

### macOS and Windows

On macOS the runner is `/usr/bin/sandbox-exec -p <policy> -D<KEY>=<value>`, with the path hard-coded and a comment saying why: only consider `/usr/bin/sandbox-exec` "to defend against an attacker trying to inject a malicious version on the PATH". The policy is four `.sbpl` fragments totalling 343 lines (116, 31, 8 and 188), starting from `(deny default)`. Paths are passed as parameters, not spliced into the text, and each writable root becomes a rule that requires the path to be inside the root and not inside an excluded subpath or a protected name. A comment explains why both a literal and a subpath form are needed: `subpath` alone leaves a gap for first-time creation of the protected directory itself, such as `mkdir .codex`. Anchor rules deny unlinking a writable-root directory so that the next policy can still be built, and the preferences fragment is included only when filesystem reads are unrestricted, because preference IPC can expose data outside the read roots.

On Windows there are two backends. The restricted-token one creates a token with three flags (`DISABLE_MAX_PRIVILEGE`, `LUA_TOKEN`, `WRITE_RESTRICTED`), runs commands under managed sandbox accounts with ACLs on the filesystem, isolates the desktop and defends against reparse points, and uses 12 Windows Filtering Platform filters on the offline identity (ICMP, DNS, DNS-over-TLS and two SMB ports, over IPv4 and IPv6). The newer one routes into Microsoft's MXC container, and its README names two limitations it accepts: hard-link names keep independent path permissions, so an allowed name can change the same file as a denied one, and volume-root expansion does not reconcile aliases, so a writable volume root plus a read-only junction can produce a writable grant on a subpath. A deny path needs a native capability or the command fails before launch, and command failures never trigger a fallback to another backend. When no Windows backend is enabled, managed filesystem restrictions are, in the code's words, only a policy shape, and unmatched commands prompt.

All platforms also call `process-hardening` before `main`: on Linux it sets `PR_SET_DUMPABLE` to zero, caps core dumps at zero and strips environment variables beginning with `LD_`; on macOS it denies debugger attach and strips `DYLD_*`.

## Approvals, rules and a second model

The approval policy has four values: `UnlessTrusted`, `OnRequest` (the default), `Granular` and `Never`. `Granular` carries five booleans (sandbox approval, rules, skill approval, permission requests, MCP elicitations), and a false one means that category is auto-rejected instead of shown. The reviewer is `user` by default or `auto_review`, which sends the same requests to a model. Every shell-like tool goes through the `ToolOrchestrator`, whose own doc comment says it "coordinates tool approval, sandbox selection, and retries after sandbox denial".

For a command no rule matched, the answer comes from a short table (`core/src/tools/../exec_policy.rs:796-880`). Try it.

<ApprovalGrid />

The orchestrator's loop is: approval, first attempt under the selected sandbox, and on a sandbox denial a set of guards (`orchestrator.rs:436-500`) that decide whether to retry. The notable one is a comment: under `Never` or `OnRequest` the harness does not retry without the sandbox, and surfaces a concise denial that preserves the original output, with one exception for a blocked network request under `OnRequest`. In practice the model has to ask to leave the sandbox itself, and the user decides. The built-in danger check is narrow: on POSIX it flags `rm` with a force option, looking through `sudo`, `env`, `trap` and nested shell scripts to a wrapper depth of 8; Windows has a much larger classifier. Everything else rests on the sandbox, on rules the user wrote, or on the reviewer.

### Rules are prefixes, and the shell decides whether they apply

The rule language is Starlark with two statements. `prefix_rule(pattern=[...], decision=..., justification=..., match=[...], not_match=[...])` matches tokens in order, any element can be a list of alternatives, and the decision is `allow` (the default), `prompt` or `forbidden`. The `match` and `not_match` examples are unit tests validated at load time. `host_executable(name=..., paths=[...])` limits which absolute paths may resolve through a basename rule. Across every matched rule and every segment of a compound command, the strictest decision wins. The repository ships no default rule file: the user's is `$CODEX_HOME/rules/default.rules`, and the only shipped example is marked "not recommended for actual use".

I loaded that example into toy 4, ported the matching and the command-splitting rule, and ran 32 commands: 24 allowed, 6 asking, 2 forbidden, and 11 of them explicitly allowed in every segment, which is the condition for being allowed to skip the sandbox (`exec_policy.rs:468-481`). The first version of this piece said 10: the real shell parser accepts a trailing `;`, so `bash -lc 'ls; '` is a plain command, which my scanner had called opaque. Both the toy and the lab above are fixed.

<ExecpolicyLab />

The result worth remembering is about shape, not strictness. A `bash -lc` script is split into plain commands only if it is words, quoted strings and numbers joined by `&&`, `||`, `;` or `|` (`shell-command/src/bash.rs:29-52`). Anything else, a command substitution, a redirection, a background `&`, a leading `VAR=x` assignment, makes the whole script one opaque command that no prefix rule matches. A newline between commands counts as a separator, which my first scanner missed. The second parser in that file, which does accept complex syntax, is documented as suitable "for identifying dangerous literal commands, but must not be used to prove that a command is safe", and it feeds only the `rm` check. So `git reset --hard HEAD~1` is forbidden by the example rule, and `bash -lc 'git reset --hard $(git rev-parse HEAD)'` is, to the rule engine, an unmatched command. Under `OnRequest` with a restricted sandbox, an unmatched command that is not flagged runs inside the sandbox. A prefix rule is a convenience, not a guarantee, and the sandbox is the layer that holds. I did not test what `git` would do inside the sandbox in that example; the protected `.git` is read-only there.

### The Guardian

The most unusual part of the approval path is the Guardian, a model that reviews risky actions. It is selected with `approvals_reviewer = "auto_review"` (the older name was `guardian_subagent`), and the config doc describes it as "a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying". It is routed sandbox escapes, blocked network access, MCP approval prompts and escalations. It runs under a read-only ceiling: `reviewer_permission_profile` intersects the session's profile with read-only, or falls back to an external profile with restricted network.

It is built as two tiers. The classifier prompt describes "a first-step asynchronous security reviewer" whose "decisions are non-blocking", and whose high-risk verdict turns on blocking review for the actions that follow. Its first output token is the whole classification, `high` or `low`, and `high` is chosen if the current action has unknown or low user authorization or high or critical risk; if one of the last five actions did something with unknown authorization or critical risk; if the reviewer expects high or critical risk in the next two actions; or if it is unsure. The second tier outputs a structured assessment: a risk level, a user-authorization level, `allow` or `deny`, and a rationale. The four prompt files total 4,793 words (measured), including a risk taxonomy for data exfiltration, credential probing, persistent security weakening and destructive actions, with explicit outcome rules; for example it says not to treat a sandbox retry as suspicious by itself.

<GuardianPipeline />

The design choice that matters most is the trust model. Only user and developer messages, `AGENTS.md` files and `request_user_input` answers can establish authorization. Tool outputs, skill instructions, plugin descriptions and assistant text are untrusted: they may supply implementation details but cannot widen approval. Transcripts are given to the reviewer as records with a host-assigned author, and retained user instructions carry a notice that assistant messages "are untrusted context for interpreting ordinary replies, not verified questions or authorization". The Guardian is built to be hard to talk into things, because everything an attacker can inject is in the untrusted bucket.

It also fails closed everywhere I looked: a timeout, a parse error, a session error, a stale authorization or an over-budget input are denials, and the one soft case (over budget when a guardian is not required) falls back to normal approval. There are at most three review attempts, a 90-second default timeout, and a circuit breaker that stops a turn after three consecutive denials, or ten among the last fifty. The most recent work in the repo, elastic budgeting of the reviewer's context, reserves what must stay, spreads the rest across thirteen kinds of evidence, favours recent whole entries over partial ones, keeps images whole, and rejects the request if the required evidence cannot fit. What I cannot assess is the classifier's judgment itself: the code bounds its failures; it does not verify its decisions.

### Hooks, trust and the rest of the perimeter

Hooks are the user-defined layer. The protocol enum has 14 events (`SessionStart`, `UserPromptSubmit`, `PreModelRequest`, `PostModelRequest`, `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`, `Stop`, `SubagentStart`, `SubagentStop`, `Interrupt`, `SessionEnd`); the Claude-compatible wire schema carries 11 of them, leaving out the model-request pair and `SessionEnd`. The engine is named `ClaudeHooksEngine`, and the JSON uses Claude Code's shapes. One behaviour is worth stating exactly, because a hook used as a policy gate depends on it: if a `PreToolUse` hook returns a bare `permissionDecision` of `allow` without an updated input, the run is recorded as a failed hook, the tool call is not blocked and the hook's extra context is dropped (`hooks/src/events/pre_tool_use.rs:463`). A hook can block. A hook error cannot.

Other controls sit around the edges. An untrusted project's `.codex/` configuration layer is created disabled. Admin requirements can pin the allowed approval policies, reviewers, sandbox modes and permission profiles, and override local settings. The executor, `exec-server`, speaks JSON-RPC over WebSocket, stdio or a relay, and authentication is opt-in: its README says omission preserves unauthenticated behaviour and to use a protected transport for remote access. Reports go through a Bugcrowd program (`SECURITY.md`).

What the repository itself says is not covered, in its own READMEs: proxy DNS rebinding, the inherit-PID mode, the two Windows container limits above, the WSL1 gap, an unauthenticated executor unless configured, and hooks that fail open on unsupported decisions. I add one of mine: the built-in danger check names one command. That is a design, not a defect: the sandbox is meant to be the protection.

## Next to Claude Code's sandbox

This site has already taken apart the other major open sandbox for coding agents, Anthropic's [sandbox-runtime](/articles/sandbox-runtime) (the layer behind Claude Code's Bash tool) and, in more depth, [its source](/articles/claude-code-sandbox-internals). The two share a skeleton, and the table puts them side by side. The Claude column is from those articles, not from a new reading.

<SandboxCompare />

Both build one bubblewrap command around a read-only root, re-mount the writable roots on top, mask denied paths, unshare user, PID and network namespaces, mount a fresh `/proc` and drop all capabilities. The differences are in what each puts around that. Codex pairs the box with a policy engine, an optional reviewing model and an executor protocol; the Claude runtime is a library that enforces whatever policy its host gives it. Codex protects four whole directory names under every writable root and creates missing ones as empty read-only targets; the Claude runtime masks fourteen specific files and directories. Codex allows `AF_UNIX` sockets in its restricted mode, where the Claude runtime denies them; Codex adds an in-process capability check that panics if any capability is left. And Codex has a Windows story and a proxy that can inspect TLS and re-sign credentials, which the Claude runtime does not.

The seccomp difference is the one I would flag. The Claude runtime ships two filters, one for sockets and `io_uring` and a second for namespaces and mounts: `unshare` and `clone` with namespace flags, `setns`, `mount`, `umount2`, `pivot_root` and the newer mount API, with `clone3` answered with `ENOSYS` on purpose. Its source article calls that second filter load-bearing for that design. The Codex filter has no counterpart: measured by grep, it contains no rule for any of those calls, and Codex never passes `--disable-userns`. What Codex has instead, read in the source, is the same bubblewrap base, a user namespace of its own, read-only bind mounts, all capabilities dropped and a helper that verifies the capability sets are empty before exec.

I did not test what the absence means in practice, and I am making no claim that it is exploitable. Whether a nested user namespace can weaken a bind mount's read-only flag depends on how the kernel locks inherited mounts, and that is something to settle with a test on a real kernel, by the maintainers or by anyone who can run the sandbox, not by a grep. I have reported nothing: this is a difference in design between two projects, found by reading.

## One engine, many front ends

The loop above does not know whether it is talking to a terminal. Every front end speaks to it through the app-server protocol, a JSON-RPC dialect that the repository generates from Rust types. I counted it from the schema sources: 183 client requests (74 of them marked experimental), 11 server requests, which are the harness asking the client for something such as an approval, and 87 server notifications. The core `Op` and `EventMsg` enums sit one layer below, with 32 and 86 variants, and the app-server maps between the two.

<ProtocolCensus />

The consequence is that the terminal UI, the desktop and IDE clients, `codex exec` and the SDKs are different skins on one conversation engine. The TypeScript SDK does not reimplement anything: it spawns `codex exec --experimental-json` and reads events from the child's output. The Python SDK is generated from the protocol schemas. The same protocol is what lets an executor run on another machine, and it explains why the unauthenticated-by-default executor noted earlier matters: the protocol is the product surface.

### Skills: instructions that load themselves

A skill is a directory with a `SKILL.md` whose frontmatter carries a name and a description. Skills are discovered from several scopes (repository, user, admin and system), the model sees a catalog of names and descriptions in its prompt, and the body of a skill is read only when the model decides it applies or the user mentions it with a `$name`. That is progressive disclosure: the cheap catalog is always present, the expensive text arrives on demand, and the catalog itself is budgeted.

<SkillLife />

The skills extension is the largest of the 18 `ext/*` crates, at 30,230 lines. The catalog is delivered as a world-state section like any other, so a skill added mid-session reaches the model as a small diff and not as a rewritten head of the request. A feature flag for catalog deltas is still under development in the version I read.

### MCP and plugins

MCP is handled by two crates: `rmcp-client`, a wrapper on the Rust MCP SDK, and `codex-mcp`, which holds server configuration, tool naming and the connection manager. Tools are exposed to the model as `mcp__server__tool`, resources through `list_mcp_resources` and `read_mcp_resource`, and discovery of deferred tools through `tool_search`. An elicitation from a server can be routed to the user or, under the granular approval policy, auto-rejected. Plugins are the packaging layer: a plugin can carry skills, MCP servers, apps and hooks, and its description reaches the model through the same world-state mechanism.

## The extension seam

The 18 crates under `ext/` are where the repository keeps features it wants out of the core loop. Each implements one or more extension traits that the core calls at fixed points, so a feature can add context fragments, tools, hooks or approval handling without a patch to `run_turn`.

<ExtensionDoors />

I did not read all 18 closely. I read the interfaces and the largest implementation, and I count the rest by size only. The pattern is the useful part: the core is deliberately small, 52 crates deep in fan-out but with the policy-heavy parts sitting beside it, not inside it.

### Code mode: the model writes a program instead of a call

The most unusual tool in the table is `exec`. In code mode the model is given a JavaScript runtime, run in a V8 isolate, and writes a short program that calls the other tools as functions, with `wait` for yielding and polling long work. The point is fewer round trips: a loop over twenty files is one tool call and not twenty. The ordering rules still hold, because the nested calls go through the same handlers, the same approvals and the same sandbox.

<CodeModeCutaway />

One precedence detail is easy to get wrong from the flags alone. Whether a turn is code-mode-only is decided by the catalog's `tool_mode` for the model, and the catalog wins over the feature flags: eleven of the twelve catalog models are code-mode-only, so for them the `code_mode` flags, which are under development, do not turn it on or off. I found that by following the resolution code, not from documentation, and I did not run a turn to confirm it.

### Two hundred and one flags

Codex ships with 201 feature flags, parsed from `features/src/lib.rs`. Their stages are the whole development process in one table: 85 under development, 61 stable, 47 removed, 4 experimental and 3 deprecated, plus one whose stage depends on the platform.

<FlagFunnel />

A removed flag is a feature that graduated into the code and had its switch deleted. Of the 47, ten are still on by default for compatibility. The figure that would have been easy to get wrong is the one I corrected during the work: my first count was 195, with 60 stable; the parse of the enum gives 201 and 61.

## What running it showed

After an earlier version of this piece went up, I built and ran the parts of the repository that can be built on their own. Cargo resolves the entire 166-crate workspace before it builds anything, and fetching its git dependencies (the V8 sources alone passed three gigabytes) does not fit on this machine, so I did not build the workspace. I built a scratch workspace holding unmodified copies of three groups of crates, pinned by the repository's own `Cargo.lock` and toolchain (Rust 1.95.0), at the same commit `67f0994b`:

- `codex-execpolicy`, the rule engine and its `codex-execpolicy check` command;
- `codex-apply-patch`, the patch parser and applier, with the real `apply_patch` binary;
- the shell-command crate's `bash.rs` (the tree-sitter splitter) and its dangerous-command check.

One piece is not Codex's: `apply-patch` reaches the disk through the executor's file-system trait, and the real implementation lives in the large `exec-server` crate, so I stood in a stub with the five methods the patch applier calls, over plain `tokio::fs`. The scripts, the stub, the logs and the raw results are in `public/articles/codex-harness-from-the-source/verify/`; its `README.log` has the exact commands.

**The crates' own tests pass (measured).** `codex-execpolicy`: 7 unit and 29 integration tests passed, 0 failed. The shell-command modules: 82 tests passed. `codex-apply-patch`: 65 of 68 unit tests passed. The three failures are mine: two expect a write to a read-only directory to fail, which does not happen when you are root, as I was, and one needs the real file layer's handling of symlinks, which my stub ignores. They are not findings about Codex.

**The patch matcher: the port was right (measured).** I fed all eighteen of toy 1's patches to the real binary in a fresh directory each and compared the final file contents with the port's. Seventeen matched byte for byte. The eighteenth, a write that fails halfway through a two-file patch, cannot be reproduced as root, so I report it as not run. The four edits that land in the wrong place, the ones I called the worrying result, are the same four in the real code: a hunk with no context that matches the first of two identical lines, a hunk without the file's indentation, a trailing-space line that loses to an exact match in another function, and two lines that differ only in nesting. This was the claim most likely to be wrong, and it held, which makes the finding more solid than it was when it rested on my port.

**Where the toy was wrong: where the refusals live (measured).** The real `apply_patch` binary applies a patch hunk by hunk with no check beforehand. The all-or-nothing behaviour I described belongs to a separate step, `maybe_parse_apply_patch_verified`, which reads every file, derives every update and writes nothing. I called it on the same patches. It rejected every patch that should fail (a missing context line, missing old lines, the two-file patch whose second hunk does not match) and it also rejected the patch that names one path twice. The standalone binary, run on the two-file patch, wrote the first file and then failed; run on the same-path patch, it applied both hunks and succeeded. So the pre-approval step is what protects a multi-file patch, and the harness runs it first (`core/src/tools/handlers/apply_patch.rs:436`; I read the call site, I did not run the harness). Anyone who calls the applier directly gets the partial-write behaviour that my toy labelled "apply loop alone".

**The rule engine: agreement on the rules, three misses in the splitter (measured).** I ran `codex-execpolicy check` with the repository's own example rule file on every command and every segment of toy 4's 32 commands: 37 segments, and the decision the real engine returned matched the toy's rule decision on all 37, with no rule matched where the toy had none. The real engine returns "no match" for an unmatched command; the "allow, inside the sandbox" default is added by `exec_policy.rs` in the core crate, which I did not run. An absolute path such as `/usr/bin/ls -l` matches nothing by default and matches the `ls` rule only with `--resolve-host-executables`. I then ran the real splitter and the real danger check. The danger flag agreed with the toy on every command. The splitter differed on one: `bash -lc 'ls; '`, which the real parser reads as the plain command `ls`. That changed the tally of commands that may skip the sandbox from 10 to 11. A second pass over ten more scripts found two more differences; all three are fixed in the toy and in the lab above: a newline between commands is a separator, and a leading `VAR=x` assignment makes the script opaque. After the fixes the toy agrees with the real splitter on all ten.

**The constants (read, not run).** I could not run the core crate, so the compaction arithmetic and the lock rules are still read from source. I re-read the formulas for the usable window and the auto-compact limit and found two things the first version omitted, a scope setting and a fallback buffer, both added above. The retry backoff constants were re-read, which corrected "up to ten percent" to plus or minus ten percent.

## What I ran and what I did not

Everything in the sections before the one above is from reading source at one checkout, plus four small programs of my own. They are toys, labelled as such, and they are in the repository next to this article. Three of them, 1, 3 and 4, were later checked against the real code, as above.

- **Toy 1, apply_patch.** A Python port of the matcher, the chunk parser and the apply loop, run on eighteen constructed patches. It shows what can happen, not how often. Checked against the real binary: 17 of 18 identical, one not runnable.
- **Toy 2, the scheduler.** A model of the per-turn read-write lock with parallel and serial tool calls, to show the completion order against the drain order. It models the rule; it does not time the real runtime. Not checked.
- **Toy 3, compaction thresholds.** Arithmetic over the real `models.json`, using the formulas from the source. Formulas re-read, not run.
- **Toy 4, execpolicy.** A port of prefix matching and the command splitter, run on 32 commands with the shipped example rule. Checked against the real engine and parser; three splitter errors fixed.

I still did not build or run the Codex agent, its core crate, the orchestrator or its sandbox. The sandbox claims (the bubblewrap argument list, the seccomp filter, the absence of rules for `unshare` and `mount`) are read and counted, not run: they need the Linux sandbox crate and a kernel I can test on. No claim about speed, the quality of the model's decisions, or what the sandbox stops in practice rests on running the product.

Counts such as 5,719 Rust files, 2,222,503 lines, 22,217 test attributes, 117 top-level crates, 166 workspace members and 584 dependency edges are from scripts over the checkout, and the dependency graph was rebuilt independently from the manifests as a cross-check. About forty-five specific claims were re-verified against the source after the first notes were written; the corrections that came out of that are in the text, among them the flag count and the hook-event count, which is 14 in the protocol and 11 on the Claude-compatible wire.

## What I could not establish

- **Whether the repository is a mirror.** The history carries 4,857 `GitOrigin-RevId` trailers across 6,300 commits, which is how an exporter from an internal monorepo would look. That is an inference. The repository does not say it.
- **What is not open.** The model weights, the hosted service, the fleet that runs cloud tasks and the ChatGPT backend are not in the repository. The post that started this is right in substance, but it is not news: the repository was created on 2025-04-13 under Apache-2.0.
- **How well any of it works.** The Guardian's decisions, the matcher's behaviour on real model output, the cache hit rates under incremental tools, the cost of the code-mode round-trip savings: none of those can be read from source.
- **Whether the seccomp difference matters.** Stated above, measured by grep, not tested. This is the one result a reader with a Linux machine could settle in an afternoon.
- **The date and shape of the next version.** The repository moves 45 to 79 commits on a normal weekday. Everything here is a photograph of one day.

## Sources

The code is at [openai/codex](https://github.com/openai/codex); the card below is a pinned snapshot, so the numbers do not restate themselves when someone pushes.

<RepoCard repo="openai/codex" />

The post that prompted this is [on X](https://x.com/awe0_io/status/2116145533782040973). Related reading on this site: [how Claude Code's sandbox is built](/articles/claude-code-sandbox-internals), [the sandbox runtime](/articles/sandbox-runtime), and [prompt caching in coding agents](/articles/prompt-caching-harnesses).
