# OpenAI Codex source analysis (deepened): a local coding agent taken apart as an engineering system you can check point by point

> **Subject**: [openai/codex](https://github.com/openai/codex) (**Codex CLI**; ≠ ChatGPT web Codex Web)
> **Based on commit**: `bb1af235ea2822d7a40f75ef52e4d6a2cde84da2` (2026-07-28, `Load thread titles concurrently during session startup (#35779)`)
> **Analysis date**: 2026-07-28 · **Document version**: **deepened edition · including scenario walkthroughs (Chapters 23–28)** (rewritten as “one point, one exposition”; target granularity aligned with the Open Design / Claude Code deep editions)
> **License**: Apache-2.0 (`LICENSE`, Copyright 2025 OpenAI)
> **Scale**: `codex-rs/` ≈63MB · **2783** `.rs` files · Cargo workspace **126** members · `session/turn.rs` **2741** lines · `session/mod.rs` **4154** lines
> **npm**: `@openai/codex` (`codex-cli/package.json`) → `bin/codex.js`
> **Method**: every key point is fixed four beats — **① what this point is solving → ② what the source looks like (`file:line`) → ③ why it is designed this way → ④ reader checklist**. Line numbers are not invented; uncertain places are marked.
> **Companions**: [three-way deep comparison](Claude-Code-vs-OpenCode-vs-Codex-深度对比.md) · interactive explainer [`openai-codex-解析.html`](../openai-codex-解析.html) (**5 runnable benches**)
> **Reference tree**: `参考项目/codex/`

---

## Contents

**Part I · What it is**
1. [Project overview: nail the product boundary first](#ch1)
2. [Panorama architecture: 126 crates and “resist core bloat”](#ch2)
3. [Startup and multiple entries: MultitoolCli command by command](#ch3)

**Part II · The heart (most important)**
4. [Thread · Session · task system](#ch4)
5. [Outer loop: RegularTask](#ch5)
6. [Inner loop: run_turn step by step (upper) — entry to sampling](#ch6)
7. [Inner loop: run_turn step by step (lower) — after sampling and stop](#ch7)
8. [run_sampling_request and try_run_sampling_request](#ch8)
9. [The compact family: pre / mid / previous-model](#ch9)
10. [The six context-window constitutions](#ch10)

**Part III · Hands and gates**
11. [Tool assembly: ToolSpec · built_tools · Router](#ch11)
12. [Orchestrator: approval → sandbox → attempt → retry](#ch12)
13. [Shell path: from arguments to ExecApprovalRequirement](#ch13)
14. [apply_patch: handler + arg0 second personality](#ch14)
15. [Sandbox three platforms and Landlock rules](#ch15)
16. [AskForApproval and approval cache](#ch16)

**Part IV · Context and orchestration**
17. [AGENTS.md discovery algorithm line by line](#ch17)
18. [Skills · Plugins · Hooks · sub-agents](#ch18)

**Part V · UI and outward face**
19. [TUI · Exec · Doctor](#ch19)
20. [App-server · MCP · Rollout](#ch20)
21. [Auth and model channel](#ch21)

**Part VI · Summary**
22. [Distinctive designs, trade-offs, how to read, comparisons](#ch22)

**Part VII · Scenario-level step-by-step walkthroughs (connect the points into stories)**
23. [Scenario: executing `rm -rf build` in an interactive session](#ch23)
24. [Scenario: a multi-file apply_patch](#ch24)
25. [Scenario: mid-turn compact when the context window is about to blow](#ch25)
26. [Scenario: a stop hook demands continue-running](#ch26)
27. [Scenario: `codex exec` + Never in CI](#ch27)
28. [Scenario: AGENTS.md chain vs override — who wins](#ch28)

---


<h2 id="ch1">Chapter 1 · Project overview: nail the product boundary first</h2>

### Point 1.1 · “Local CLI” and “Codex Web” are not the same thing

**① What it solves**
Users searching “Codex” hit three things: the ChatGPT web cloud Agent, the IDE extension, and this repo’s CLI. Mixing them produces the useless question “why is there no web session page in the source.”

**② What the source / docs look like**
Root `README.md` is explicit: CLI runs locally; IDE is linked elsewhere; Cloud goes to chatgpt.com/codex. This analysis covers only **the local runtime implemented in this monorepo**.

**③ Why**
OpenAI uses one brand across multiple surfaces, but the engineering repo only carries the open-sourceable local machine library. The cloud product need not, and should not, appear in this tree.

**④ Checklist**
- [ ] Are you analyzing the `codex` binary or the web page?
- [ ] Are you mistakenly treating app-server as “the complete web backend”?

### Point 1.2 · One-sentence engineering definition

**Codex CLI = a Rust Agent runtime + Responses API tool surface + OS sandbox + AGENTS.md instruction loading + rollout session assets.**

Taken apart:

| Part | Lands at |
|---|---|
| Runtime | `codex-rs/core` (Session / turn) |
| Tool surface | `codex-rs/tools` + `core/tools/handlers` |
| Sandbox | `sandboxing` + `linux-sandbox` + macOS Seatbelt + Windows Token |
| Instructions | `codex-rs/core/src/agents_md.rs` |
| Session assets | `rollout` |
| Facade | `cli` / `tui` / `exec` / `app-server*` |

### Point 1.3 · License and distribution

- Apache-2.0 · Copyright 2025 OpenAI (`LICENSE`)
- Install: `curl …/codex/install.sh` · `npm i -g @openai/codex` · brew cask
- The npm package is only a thin wrapper: `codex-cli/package.json`’s `bin.codex` → `bin/codex.js`; the real logic is in the Rust binary

### Point 1.4 · “Species difference” from neighbors in this series

| Neighbor | Relation |
|---|---|
| Claude Code | same-class product (local coding Agent), closed-source, full tool family |
| OpenCode | same-class open source, Bun/Effect, the server *is* the body, multi-provider |
| Open Design | **host**: treats `codex` as one engine; does not write the loop itself |
| MiMo-Code | OpenCode fork; tool ABI leans toward Codex style |

```mermaid
flowchart LR
  Brand["Codex brand"] --> CLI["this-repo CLI"]
  Brand --> IDE["IDE extension"]
  Brand --> Web["Codex Web cloud"]
  CLI --> Core["codex-rs"]
```

> 🧠 **One sentence**: first admit “the brand is large, the repo is specialized”; every later chapter only stands because of that.

---


<h2 id="ch2">Chapter 2 · Panorama architecture: 126 crates and “resist core bloat”</h2>

### Point 2.1 · Why split into a hundred-plus crates?

**①** A large Rust product uses crate boundaries instead of “folder convention,” forcing dependency direction.
**②** The `members` list in `codex-rs/Cargo.toml` is about 126 items: `core`, `tui`, `cli`, `tools`, `protocol`, `sandboxing`, `apply-patch`, `rollout`, `login`, `app-server*`…
**③** Root `AGENTS.md:72-83` **explicitly resists** stuffing more into `codex-core` — admitting history is already bloated.
**④ Check**: should a new feature go into a new crate or core? Official policy’s answer is the former.

### Point 2.2 · Four-flight staircase

```mermaid
flowchart TB
  subgraph L1["Facade layer"]
    CLI["cli Multitool"]
    TUI["tui"]
    EX["exec"]
    AS["app-server*"]
  end
  subgraph L2["Session layer"]
    TH["CodexThread"]
    SE["Session"]
    TK["tasks/*"]
  end
  subgraph L3["Turn layer"]
    RT["run_turn"]
    SR["run_sampling_request"]
    OR["ToolOrchestrator"]
  end
  subgraph L4["Side-effect layer"]
    HD["handlers"]
    SB["SandboxManager"]
    FS["exec-server FS"]
  end
  CLI --> TH
  TUI --> TH
  EX --> TH
  AS --> TH
  TH --> SE --> TK --> RT --> SR --> OR --> HD --> SB
```

### Point 2.3 · Size signals (this machine’s checkout)

| Path | Signal |
|---|---|
| `tui/` ~14MB | the interactive surface is the product facade |
| `core/` ~11MB | the war room; policy says do not stuff more |
| `turn.rs` 2741 lines | single-file heart |
| `session/mod.rs` 4154 lines | Session monolith |
| `orchestrator.rs` 533 lines | where the approval+sandbox state machine is concentrated |

### Point 2.4 · Engineering culture (must-read to understand the architecture)

Root `AGENTS.md` also stipulates: module target <500 LoC, split if over 800; add few methods to TUI center files; `just test` not bare `cargo test`; context injection must use `ContextualUserFragment`.
**These are not style-guide decoration; they are the key to understanding “why the files look like this.”**

---


<h2 id="ch3">Chapter 3 · Startup and multiple entries: MultitoolCli command by command</h2>

### Point 3.1 · Default path: no subcommand → TUI

**②** `cli/src/main.rs:90-119`: `MultitoolCli`; `bin_name = "codex"`; `subcommand_negates_reqs = true`; field `interactive: TuiCli`.
**③** User mental model is “type `codex` and chat”; ops capabilities unfold as subcommands, not polluting the default.

### Point 3.2 · Subcommand classification (`Subcommand` about lines 122–211)

**A. Run tasks**
- `exec`/`e`: non-interactive
- `review`: non-interactive review

**B. Identity**
- `login` / `logout`

**C. Extension surface**
- `mcp` / `mcp-server`
- `plugin`
- `app-server` / `remote-control` / platform `app`

**D. Session assets**
- `resume` / `fork` / `archive` / `unarchive` / `delete`

**E. Security and diagnostics**
- `sandbox`: run a command in the sandbox (`HostSandboxArgs`)
- `doctor`
- `debug` (`DebugCommand`)
- `execpolicy` (hidden)

**F. Other**
- `apply`/`a`: `git apply` an agent diff
- `cloud`: cloud tasks (experimental)
- `features` / `update` / `completion`
- Hidden: `responses-api-proxy`, `stdio-to-uds`
  > Note: `exec-server` is marked with a `/// [EXPERIMENTAL]` doc comment at `main.rs:206-207`, but **has no `hide = true`**, so it is a publicly visible subcommand and should not be listed in the hidden group (verification correction 2026-08-04).

### Point 3.3 · Design trade-offs

| Trade-off | Meaning |
|---|---|
| Swiss-army-knife CLI | high learning cost, but the ops closed loop is all in one binary |
| Hide dangerous/internal commands | reduce misuse; when analyzing, compare `--help` with source |
| macOS/Windows conditional compile `app` | desktop is not first-class on every platform |

```mermaid
flowchart LR
  A["codex"] --> B{"subcommand?"}
  B -->|none| C["TUI"]
  B -->|exec| D["non-interactive turn"]
  B -->|resume| E["rollout restore"]
  B -->|mcp-server| F["be MCP itself"]
  B -->|sandbox| G["sandbox debug"]
```

---


<h2 id="ch4">Chapter 4 · Thread · Session · task system</h2>

### Point 4.1 · CodexThread is the facade

**①** UI/protocol should not directly touch Session internal locks.
**②** `codex-rs/core/src/codex_thread.rs:182` `struct CodexThread`; `submit` around line 225 hands the `Op` in.
**③** Facade pattern lets TUI / app-server / exec share the same submit semantics.
**④** Finding “the first stop after the user hits send” → follow from Thread.submit.

### Point 4.2 · Session.submit family

From `codex-rs/core/src/session/mod.rs:793`:

- `submit(op)`
- `submit_with_trace`
- `submit_with_id`
- `submit_user_input_with_client_user_message_id` (813+)

The same session also has `active_turn`, `input_queue`, services (model client, exec policy, plugins…).

### Point 4.3 · Interjection and idle start (inject)

`codex-rs/core/src/session/inject.rs`:

- `inject_if_running`: while the model is running, stuff input into the current turn
- `try_start_turn_if_idle`: only start a new turn when idle; Plan mode refuses some automatic turns (about lines 89–92)

**③** This explains why “type while it is running” does not drop characters in the product, and why Plan is more “quiet.”

### Point 4.4 · Tasks are not only Regular

Under `tasks/` there are also compact, review, and other task types (`RegularTask` is in `codex-rs/core/src/tasks/regular.rs`). Session uses `spawn_task` to hang different `SessionTask` implementations.

---


<h2 id="ch5">Chapter 5 · Outer loop: RegularTask</h2>

### Point 5.1 · The file is short; its status is extremely high

`codex-rs/core/src/tasks/regular.rs` is about a hundred lines in full, but it is the adapter from **UI turn → run_turn**.

### Point 5.2 · run() step by step

| Step | Lines about | What it does |
|---|---|---|
| 1 | 47–58 | emit `TurnStarted` (including mode, context window, trace) |
| 2 | 59 | `set_server_reasoning_included(false)` |
| 3 | 60–74 | consume startup prewarm: Cancelled→early return; Ready→bring prewarmed session |
| 4 | 77–92 | **outer loop**: `run_turn`; if no pending, return; if pending, `next_input=[]` and run again |

### Point 5.3 · Two-layer loop comparison table (memorize)

| Layer | Location | Why it loops |
|---|---|---|
| Outer | RegularTask | the user interjected again / there are still trigger items in the queue |
| Inner | run_turn | the model still wants tools / stop hook continue-runs / compact then continue |

```mermaid
stateDiagram-v2
  [*] --> Prep: TurnStarted + prewarm
  Prep --> Inner: run_turn
  Inner --> Inner: pending input
  Inner --> [*]: no pending
```

### Point 5.4 · Two details you only see in source

The outer loop in full is just these few lines (`codex-rs/core/src/tasks/regular.rs:76-92`), but two places are worth pointing out separately:

```rust
let mut next_input = input;
loop {
    let last_agent_message = run_turn(
        ..., next_input,
        prewarmed_client_session.take(),          // ← ①
        cancellation_token.child_token(),         // ← ②
    ).await?;
    if !sess.input_queue.has_pending_input(&sess.active_turn).await {
        return Ok(last_agent_message);
    }
    next_input = Vec::new();                      // ← ③
}
```

**① `.take()`: prewarm is only for the first lap**
`prewarmed_client_session` is `Option`; `.take()` empties it. So **only the outer loop’s first `run_turn` gets the prewarmed connection**; every later lap is `None`, going through `sess.services.model_client.new_session()`.
> ③ Why: prewarm is to **mask cold-start latency** — the wait at the moment the user hits enter. The second lap is already in conversation; there is no first-token latency to mask, so occupying a prewarmed connection has no meaning.

**② `child_token()`: one child token per lap**
It does not pass the same `cancellation_token` down; each lap derives a child token. Parent-token cancel cascades to all children, but **cancel inside a single lap does not pollute the whole task in reverse**.

**③ `next_input = Vec::new()`: from the second lap, no new input is carried**
This is where “interjection” semantics land: the first lap runs with this user input; if after finishing it finds more in the queue (the user typed again during model output), **the next lap passes empty input** — because those interjections are already in `input_queue`, drained by logic inside `run_turn`; they must not be stuffed again from outside.

> 🧠 **Trade-off**: putting “interjection” on the outer layer rather than stuffing it into sampling retry avoids mixing **user input** and **model tool-followup** into the same control flow.
> The cost is that the outer layer pays a full `run_turn` entry overhead again (pre-compact, MCP wake, step-context freeze all start over) — **Codex chose control-flow clarity, not saving this one overhead**.

---


<h2 id="ch6">Chapter 6 · Inner loop: run_turn step by step (upper) — entry to sampling</h2>

> File: `codex-rs/core/src/session/turn.rs` (**2741** lines). Entry: `run_turn` @ **153**.

### Point 6.1 · What the function signature is declaring as a contract

```text
run_turn(sess, turn_context, turn_extension_data, input,
         prewarmed_client_session, cancellation_token)
  -> CodexResult<Option<String>>
```

The returned `Option<String>` is a “last agent message” class result, for the outer layer/hooks. `cancellation_token` runs through — cancel is not after-the-fact politeness; it is a first-class parameter.

### Point 6.2 · First thing at the door: pre_sampling_compact (167–184)

Calls `run_pre_sampling_compact` (defined @ **983**):

1. First `maybe_run_previous_model_inline_compact` — when **the model changed / compaction hash changed**, use the old model for compatibility compact (comments 1047–1056)
2. Look at `context_window_token_status`; if `token_limit_reached`, capture step_context then `run_auto_compact(..., CompactionPhase::PreTurn)`

**① What it solves**: avoid sampling while carrying already-blown-window history.
**③ Why compact first then record new input**: a TODO comment (163–166) admits the ideal is “estimate upcoming tokens then compact”; the current order is still a compromise.
**④ Check**: when you change compact thresholds, are pre-turn and mid-turn (later) semantically consistent?

### Point 6.3 · Resolve which MCP this round must wake (187–197)

`required_mcp_servers_for_input`: decide by user input which MCP to **lazy-wake**, rather than opening all every time.
On cancel, record hooks/inputs then return.

### Point 6.4 · Freeze step_context (199–214)

`capture_step_context_with_required_mcp_servers`: freeze **this step’s visible tool set, world state, config snapshot**.
Later sampling and tool calls must share the same view (comment 294: context / advertised tools / tool calls, same request view).

**③** This is “a reproducible step” — when debugging “why didn’t this round have a certain tool,” look at step_context first, not global config.

### Point 6.5 · world_state and diff roots (215–218)

`tokio::join!`: one side `record_context_updates_and_set_reference_context_item`, the other computes `turn_diff_display_roots`.
Parallelism is for startup latency; semantically both serve “this turn’s visible world.”

### Point 6.6 · skills / plugins injection (220–230)

`build_skills_and_plugins`: on failure return `Ok(None)` early — skill assembly is not optional decoration; failure means do not start the run.

### Point 6.7 · hooks and recording input (232–256)

- `run_pending_session_start_hooks`
- `run_hooks_and_record_inputs`
- merge connector selection
- record `PreviousTurnSettings` (model slug, comp_hash, realtime)
- write injection_items into the conversation

**④ Check**: when a session start hook returns “should stop,” do you mistakenly think it is an empty model reply?

### Point 6.8 · Two policy switches before entering the loop (assigned at 236 · comments 264–269)

- `can_drain_pending_input`: often false at turn start — **sample this round’s input first, then drain interjections**
- later auto-compact runs also postpone drain (comments 266–268)

```mermaid
flowchart TB
  A["run_turn enter"] --> B["pre_sampling_compact"]
  B --> C["MCP required"]
  C --> D["capture_step_context"]
  D --> E["skills/plugins"]
  E --> F["hooks + record input"]
  F --> G["loop: sampling"]
```

---

<h2 id="ch7">Chapter 7 · Inner loop: run_turn step by step (lower) — after sampling and stop</h2>

### Point 7.1 · Loop head: pending + reminders (loop from 270)

Each lap may:

1. drain pending (if allowed) and run hooks
2. `rollout_budget::maybe_record_reminder`
3. get/rebuild `step_context` (when there is pending user input, recompute MCP from the input)
4. `time_reminder::maybe_record_current_time_reminder`
5. update world_state
6. `clone_history().for_prompt(modalities)` to construct sampling input

### Point 7.2 · Call sampling (344–356)

`run_sampling_request(...)` (defined @ **1308**). On success you get:

- `model_needs_follow_up`
- `last_agent_message`

### Point 7.3 · Post-sampling state machine (357–512) — branch by branch

**Branch A · needs follow-up**

- `accept_mailbox_delivery_for_current_turn` (364–369)
- `can_drain_pending_input = true`
- compute `needs_follow_up = model_needs_follow_up || has_pending_input`
- emit a large amount of token usage trace (387–401)

**Branch B · should_roll_over (new window / token top)** (419–458)

- Condition: `needs_follow_up && (new_context_window_request || token_limit_reached)`
- `run_auto_compact(..., CompactionPhase::MidTurn)`, injection strategy `BeforeLastUserMessage`
- after compact, may run session start hooks again
- `can_drain_pending_input = !model_needs_follow_up` then `continue`

Comment 430: **believe compact can pull the window back, therefore not afraid of infinite loop** — this is an explicit product bet.

**Branch C · does not need follow-up → stop protocol** (461–510)

1. `run_turn_stop_hooks`
2. If `should_block` and there are continuation fragments → record into history, open mailbox, `stop_hook_active=true`, `continue` (the hook demands continue-running)
3. If block but no prompt → Warning then ignore
4. If `should_stop` → `break`
5. legacy after-agent hook may `Ok(None)`
6. otherwise `break`

**Branch D · errors**

- `TurnAborted` rethrown
- other errors continue to be handled in later match arms (517+)

### Point 7.4 · One “can it stop?” decision table

| Signal | Behavior |
|---|---|
| model still wants tools | continue |
| user interjection in queue | continue |
| token top and still needs to continue | mid-turn compact then continue |
| stop hook block+prompt | inject continuation continue |
| stop hook stop | break |
| none of the above | break, bring last_agent_message back |

```mermaid
flowchart TB
  S["sampling returns"] --> F{"needs_follow_up?"}
  F -->|yes| R{"should_roll_over?"}
  R -->|yes| C["mid-turn compact"] --> S
  R -->|no| S
  F -->|no| H["stop hooks"]
  H --> B{"block?"}
  B -->|yes+has prompt| S
  B -->|no| K{"stop?"}
  K -->|yes| X["break"]
  K -->|no| X
```

> 🧠 **One sentence**: Codex’s “stop” is not `if !tool_calls`; it is a three-party protocol of **follow-up × compact × stop hook**.

---

<h2 id="ch8">Chapter 8 · run_sampling_request and try_run_sampling_request</h2>

### Point 8.1 · run_sampling_request (1308–1405): the retry-bearing shell

Step by step:

1. take `base_instructions`
2. build `ToolCallRuntime` (router + sess + step_context + diff tracker)
3. start `code_mode_service.start_turn_worker` (code-mode parallel worker)
4. **retry loop**:
   - first time use the passed-in input; afterwards `for_prompt` from history
   - `build_prompt`
   - `try_run_sampling_request`
   - on success return `(output, original_input)`
   - `ContextWindowExceeded` / `UsageLimitReached`: update state then return the error
   - other: if retryable, go `handle_retryable_response_stream_error` and `record_sampling_retry`

**③** Recoverable WebSocket/stream errors do not kill the whole turn; context-window blow and quota hard errors fail immediately.

### Point 8.2 · try_run_sampling_request (2165+): the real stream receive

Key structure:

- `feedback_tags!` stamps model, approval_policy, sandbox_policy, effort, auth, features (2176–2183) — **observability first-class**
- `client_session.stream(prompt, ...)` opens the stream (2195–2208), cancellable
- `FuturesOrdered` `in_flight`: future queue for **parallel tool calls** (2209)
- `needs_follow_up` / `last_agent_message` accumulators
- Plan-mode dedicated parser and `PlanModeStreamState` (2221–2223)
- outer `loop` pulls stream events (2228+)

Whether the model supports parallel: `supports_parallel_tool_calls` (about line 1289, near the built path).

### Point 8.3 · Why two layers of sampling functions?

| Layer | Duty |
|---|---|
| `run_sampling_request` | retry, swap prompt input, quota/window errors |
| `try_run_sampling_request` | single stream lifecycle, tool concurrency, UI events |

Split, tests and tracing (`receiving_stream` span) are cleaner.

### Point 8.4 · Reader experiments

1. Simulate network drop: see whether the retryable path fires
2. Send a message in Plan mode: see whether `plan_mode` branch events differ
3. Open a model with parallel tools: observe whether `FuturesOrdered` overlaps multiple tasks

---

<h2 id="ch9">Chapter 9 · The compact family: pre / mid / previous-model</h2>

### Point 9.1 · Trigger-source comparison

| Phase | Function / call site | Reason enum |
|---|---|---|
| PreTurn | `run_pre_sampling_compact` @983 | `token_limit_reached` |
| MidTurn | `should_roll_over` inside run_turn @431 | ContextLimit + new-window request |
| Previous model | `maybe_run_previous_model_inline_compact` @1051 | comp_hash changed or smaller window |

### Point 9.2 · comp_hash_changed (1014–1019)

**Only triggers when both sides have a hash and they differ** — missing hash does not compact blindly. This is to avoid “mis-compact when information is incomplete” destroying the cache.

### Point 9.3 · Previous-model compact preconditions (1032–1040)

> ⚠️ **Verification correction (2026-08-04)**: the original draft treated lines 1032–1040 as previous-model compact’s “on/off preconditions.” That is **putting the wrong hat on**.

`turn.rs:1032-1041` is actually `capture_current_model_fallback_step_context` (defined @1026); its doc comment states its use is *"Captures the current model's request-scoped state for **retrying** previous-model compaction"* — it only decides **whether, on failure, you can get a fallback step context for retry**, not the compact switch itself; its `Ok(None)` return is only “do not capture fallback context,” not “do not compact.”

The real trigger gate is `maybe_run_previous_model_inline_compact` (`turn.rs:1057-1119`); the three trigger paths **none of them check whether the auth backend or provider is OpenAI**:
1. `comp_hash_changed` is true (compact directly @1071); or
2. `previous_model_limit_reached && slug different && old_context_window > new_context_window` (compact @1116-1118).

**Corrected conclusion**: on third-party providers, previous-model compact **still triggers as usual**; the extra Codex-backend + OpenAI judgment is only for “whether, when compact fails, you can fall back to fallback step context,” not “whether to compact.” The original sentence “do not play this compatibility compact on third-party providers” should be deleted.

### Point 9.4 · InitialContextInjection

Mid-turn uses `BeforeLastUserMessage { world_state, step_context }` — after compact, world state must be stuffed back at the correct position, avoiding “the summary covering the last user utterance.”

### Point 9.5 · Relation to the context constitution

Compact is one of the six constitutions’ executors: history only incremental, bounded, protect cache. Recklessly changing compact injection position directly violates `AGENTS.md:91-100`.

---

<h2 id="ch10">Chapter 10 · The six context-window constitutions</h2>

Root `AGENTS.md:91-100` spirit, transcribed:

| # | Rule | Engineering meaning |
|---|---|---|
| 1 | No history rewrite | forbid “stealth-editing an already-sent prefix” |
| 2 | Move the prefix little | protect prompt cache, save money, cut latency |
| 3 | Bounded + hard cap | prevent memory-leak-style infinite injection |
| 4 | Single item ≤10K tokens | hard limit |
| 5 | New items that may be >1k = P0 human review | process gate |
| 6 | Must `ContextualUserFragment` | type system forces registration |

**Contrast**

- Claude Code: CLAUDE.md collection rules + system-reminder
- OpenCode/MiMo: external MEMORY/checkpoint
- Codex: **first police the cleanliness of “model-visible history” itself**; external memory is another layer (memories crates exist, but the constitution writes about history)

**④ Checklist (for PR reviewers)**
- [ ] Does the new injection go through Fragment?
- [ ] Did it rewrite an old history entry?
- [ ] Could a single item be >1k? Is it marked P0?

---

<h2 id="ch11">Chapter 11 · Tool assembly: ToolSpec · built_tools · Router</h2>

### Point 11.1 · ToolSpec: the “menu JSON” for the Responses API

`codex-rs/tools/src/tool_spec.rs:19-53`:

| Variant | wire `type` | Meaning |
|---|---|---|
| Function | function | ordinary function tool |
| Namespace | namespace | namespace grouping |
| ToolSearch | tool_search | tool search |
| WebSearch | web_search | hosted search (external/indexed switch) |
| Freeform | custom | freeform |

`name()` @56-66; `create_tools_json_for_responses_api` @79 serializes into the request.

**③** Codex does not hide from the model “this is the OpenAI Responses tool protocol” — a parallel universe to Claude Code’s Anthropic tool blocks.

### Point 11.2 · built_tools (`codex-rs/core/src/session/turn.rs:1459+`): one turn’s tool universe

> Verification correction (2026-08-04): the original draft only wrote “1459+” without a file path. The real location is `codex-rs/core/src/session/turn.rs:1459` (not under the `tools/` directory); signature `built_tools(sess, turn_context, environments, mcp, step_store, prepared_recommendations) -> (Vec<ToolInfo>, Arc<ToolRouter>)`.

Inputs: Session, TurnContext, environments, MCP binding, extension store, prepared recommendations.
Outputs: `(Vec<ToolInfo>, Arc<ToolRouter>)`.

In between it: pulls MCP tools, merges connectors by apps_enabled, handles tool_suggest recommendation candidates, then assembles the router.
**①** “What tools the model can see this step” is computed, not a global constant.

### Point 11.3 · The handlers directory is the implementation table of “hands”

`codex-rs/core/src/tools/handlers/mod.rs` exports: Shell, ApplyPatch, ViewImage, Plan, MCP, multi_agents, unified_exec, RequestUserInput/Permissions, Plugins, ContextRemaining, WaitForEnvironment, Dynamic…
> Verification correction (2026-08-04): `CodeMode` is **not** in the `handlers/mod.rs` export table — it is `codex-rs/core/src/tools/code_mode/`, declared by `pub(crate) mod code_mode;` at `tools/mod.rs:2`, a **sibling module** of `handlers` rather than a submodule.

**Difference from Claude Code (state it clearly)**:

| | Claude Code | Codex |
|---|---|---|
| File read | often a standalone Read tool | often lands in shell / others |
| Patch | Edit/Write | **apply_patch first-class** |
| Search code | Grep/Glob | often shell; web uses hosted WebSearch |
| Parallel | product-defined | `FuturesOrdered` + model capability |

### Point 11.4 · ToolCallRuntime

Constructed in `run_sampling_request` with router+session+step+diff (1323–1328). During streaming, tool calls are dispatched to it, not scattered across match arms.

---

<h2 id="ch12">Chapter 12 · Orchestrator: approval → sandbox → attempt → retry</h2>

### Point 12.1 · The file header is the manual

`codex-rs/core/src/tools/orchestrator.rs:4-7` comments write the order in stone:

> approval → select sandbox → attempt → retry with escalated sandbox on denial (**do not re-approve**, because already approved)

This is the skeleton of the whole chapter.

### Point 12.2 · ExecApprovalRequirement three states (`codex-rs/core/src/tools/sandboxing.rs:156-175`)

| State | Meaning |
|---|---|
| `Skip { bypass_sandbox, proposed_amendment }` | no need to ask a human; first attempt may already skip sandbox |
| `NeedsApproval { reason, proposed_amendment }` | must ask a human; may attach a policy-amendment proposal “similar commands later, skip asking” |
| `Forbidden { reason }` | forbidden outright |

### Point 12.3 · default_exec_approval_requirement (198–234)

**It is two-stage; the order cannot be mixed** (easy to misread; expand):

```rust
// Stage 1: look only at policy × fs.kind, compute needs_approval
let needs_approval = match policy {
    Never                        => false,
    OnRequest | Granular(_)      => matches!(fs.kind, FileSystemSandboxKind::Restricted),
    UnlessTrusted                => true,
};

// Stage 2: when needs_approval is true, Granular gets a chance to upgrade it to Forbidden
if needs_approval && matches!(policy, Granular(cfg) if !cfg.allows_sandbox_approval()) {
    Forbidden { reason: "approval policy disallowed sandbox approval prompt" }
} else if needs_approval { NeedsApproval { .. } }
else { Skip { bypass_sandbox: false, .. } }
```

Truth table (`allows_sandbox_approval()` written as **ASA**):

| policy | FS = Restricted | FS ≠ Restricted |
|---|---|---|
| `Never` | Skip | Skip |
| `UnlessTrusted` | NeedsApproval | **NeedsApproval** (independent of FS) |
| `OnRequest` | NeedsApproval | Skip |
| `Granular` · ASA=true | NeedsApproval | Skip |
| `Granular` · **ASA=false** | **Forbidden** | **Skip** ← note this cell |

> ⚠️ **The bottom-right cell is the easiest to get wrong**: `Granular` turned sandbox approval off, but when the filesystem is **not** Restricted, the result is **Skip not Forbidden**.
> Because `Forbidden` hangs on the premise of `needs_approval` — **when you do not need to ask a human, the “must not ask a human” config does not participate in the judgment at all**.
>
> **③ The subtle point still holds, but say it accurately**: when the system **originally intended to pop a dialog**, and policy forbids popping, Codex chooses **Forbidden rather than silent allow** — preventing the mental error “I turned off the dialog = I allow everything.”
> And in scenes that originally did not need to ask, it will not refuse one extra time for nothing. **This is a precise rule, not a one-cut.**

### Point 12.4 · SandboxOverride (236–271)

`sandbox_override_for_first_attempt`:

1. If policy contains **denied-read**, forbid unsandboxed (otherwise denied-read would fail) → `NoOverride`
2. If ExecPolicy Allow and `bypass_sandbox=true` → first attempt may Bypass
3. If escalated permissions were requested → Bypass
4. otherwise NoOverride

`unsandboxed_execution_allowed` (279–283): if there is a denied-read restriction then **false**.
`sandbox_permissions_preserving_denied_reads` (285–298): when escalated meets denied-read, **drop back to UseDefault**, avoiding “raise privilege = tear down read-only deny.”

> 🧠 **One sentence**: when raise-privilege conflicts with “deny read,” Codex chooses **keep the deny-read**, not satisfy the raise.

### Point 12.5 · Approvable / Sandboxable / ToolRuntime traits (312–406)

- `approval_keys`: apply_patch can have many keys (many files)
- `should_bypass_approval`: already approved or Never
- `wants_no_sandbox_approval`: UnlessTrusted always; Never never; Granular looks at config
- `ToolRuntime::run(req, SandboxAttempt, ctx)`: actually execute

### Point 12.6 · SandboxAttempt structure (408–425)

Carries: `SandboxType`, whether sandbox was requested, permission portrait, linux-sandbox exe, legacy landlock, Windows level/private desktop, network proxy, cancellation…
**This is the complete environment manual of “one attempt.”**

```mermaid
flowchart LR
  R["Tool request"] --> A["ExecApprovalRequirement"]
  A -->|Forbidden| X["deny"]
  A -->|NeedsApproval| U["ask human / cache"]
  A -->|Skip| S["select SandboxType"]
  U --> S
  S --> T["SandboxAttempt.run"]
  T -->|sandbox deny and can escalate| S2["escalated retry no longer asks human"]
```

---

<h2 id="ch13">Chapter 13 · Shell path: from arguments to ExecApprovalRequirement</h2>

### Point 13.1 · run_exec_like overall flow (`codex-rs/core/src/tools/handlers/shell.rs:63`)

1. take filesystem, environment-variable overlays
2. whether `Feature::ExecPermissionApprovals` is on
3. `apply_granted_turn_permissions` merges rights already granted this turn
4. normalize/validate additional permissions (109–120)
5. **raise-privilege guard** (122–138): if sandbox override is requested and not pre-approved, and policy is not OnRequest → **RespondToModel deny directly** (copy says do not ask for raise-privilege under non-OnRequest)
6. **Intercept apply_patch** (140–157): if the command is a patch, hand to `intercept_apply_patch` — secretly patching inside shell is still caught onto the structured path
7. emit ToolEmitter begin event
8. `create_exec_approval_requirement_for_command` (175–190)
9. assemble `ShellRequest`, `ToolOrchestrator::run` + `ShellRuntime`

### Point 13.2 · Why must shell intercept apply_patch?

Prevent the model from using `apply_patch <<'EOF'` to walk a “pure shell string” around patch approval keys (multi-file keys) and the parser.
**A structured patch must walk a structured gate.**

**And this intercept is not only in one place.** Searching the whole repo for `intercept_apply_patch` there are only two call sites; they happen to be **the two roads on which the model can run a command**:

| Call site | What this road is |
|---|---|
| `codex-rs/core/src/tools/handlers/shell.rs:142` | classic `shell` tool |
| `codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs:314` | **unified_exec backend** (persistent shell session) |

> 🔑 **This is the complete argument**. Blocking only one road equals not blocking — the model switches backends and goes around.
> In other words: **the invariant “a structured patch must walk a structured gate” is maintained by “inserting the same interceptor at every entry that can execute a command,”** not by one tool’s self-discipline.
>
> **④ Check**: if you add a third execution backend to Codex, `intercept_apply_patch` is something that must be wired together — otherwise you open a silent patch bypass.

### Point 13.3 · Shell backend variants (tool_config.rs)

Classic shell vs unified_exec vs zsh-fork: a combination of feature flags decides the backend; comments emphasize you must not silently fall onto an un-enabled backend.
**④ Check**: when turning on zsh-fork, are feature + user shell + path all satisfied?

### Point 13.4 · Relation to the “safe command” narrative

UnlessTrusted depends on a “known-safe read-only” judgment (protocol comment @ `codex-rs/protocol/src/protocol.rs:909-911`). What actually lands on the Shell path is exec_policy + orchestrator, not the model’s self-discipline alone.

---

<h2 id="ch14">Chapter 14 · apply_patch: handler + arg0 second personality</h2>

### Point 14.1 · Independent crate

`codex-rs/apply-patch`: `parse_patch`, `StreamingPatchParser`, `Hunk`… can land on ExecutorFileSystem.

### Point 14.2 · Handler

`codex-rs/core/src/tools/handlers/apply_patch.rs` (**666** lines): implements approval keys (per file), integrated with orchestrator.
One patch, many files → many approval keys (sandboxing comments 65–70, 316–321).

### Point 14.3 · arg0 second personality (`codex-rs/arg0/src/lib.rs`)

| Mechanism | Lines about | Role |
|---|---|---|
| `APPLY_PATCH_ARG0 = "apply_patch"` | 20 | if executable filename matches, enter patch main |
| Compatible `applypatch` | 21 | prevent misspelling |
| Special argv1 | 114+ | explicit internal call |
| UNIX symlink / Windows bat | 322+ | so PATH can type it directly |

**①** Distribution cost: in the user environment “there is an apply_patch command,” but it is actually the same Codex binary.
**③** Same family as linux-sandbox’s arg0 trick — **one artifact, multiple personalities**.

### Point 14.4 · CLI `codex apply`

`git apply` the agent-produced diff onto the worktree — a human-in-the-loop “accept the patch” exit, different from the model tool path.

```mermaid
flowchart TB
  M["model"] --> T1["apply_patch tool"]
  M --> T2["apply_patch text inside shell"]
  T2 --> I["intercept_apply_patch"]
  T1 --> H["ApplyPatchHandler"]
  I --> H
  H --> O["Orchestrator"]
  O --> P["apply-patch crate lands on disk"]
  U["user"] --> A["codex apply"] --> G["git apply"]
```

---

<h2 id="ch15">Chapter 15 · Sandbox three platforms and Landlock rules</h2>

### Point 15.1 · SandboxType and platform selection

`codex-rs/sandboxing/src/manager.rs:34-73`:

```rust
None | MacosSeatbelt | LinuxSeccomp | WindowsRestrictedToken
```

`get_platform_sandbox`: macOS→Seatbelt; Linux→Seccomp; Windows looks at the switch→Token or None.

### Point 15.2 · SandboxablePreference

`Auto | Require | Forbid` (54–58) — a tool can declare whether it wants a sandbox.

### Point 15.3 · transform boundary (comments 107–110)

`SandboxExecRequest` only converts PathUri to native paths at the execution boundary (exec-server / equivalent). The orchestration layer keeps URIs — **remote execution and local execution, the same permission model**.

### Point 15.4 · Linux Landlock (`codex-rs/linux-sandbox/src/landlock.rs`)

> ⚠️ **Verification correction (2026-08-04)**: the original draft presented Landlock as Linux’s live **filesystem** sandbox; that is **the framework inverted**. Source itself says Landlock’s filesystem ruleset is currently **not enabled** (legacy/fallback path); the Linux filesystem sandbox actually running is **bubblewrap** (see 15.7).

`landlock.rs:41` original: “Filesystem restrictions are intentionally handled by bubblewrap.” `:135-136`: “this is currently unused because filesystem sandboxing is performed via bubblewrap. It is kept for reference and potential fallback use.”

So what **actually takes effect** in the Landlock module is the network/process-side seccomp constraints (`PR_SET_NO_NEW_PRIVS` + network filter, `landlock.rs:42-88`), not the FS ruleset (`:144-158`’s `/` read-only rule is currently dead code). When reading this code, do not treat `install_filesystem_landlock_rules_on_current_thread` as the main path.

### Point 15.5 · macOS Seatbelt

Root `AGENTS.md:8-10`: child processes carry `CODEX_SANDBOX=seatbelt`; tests under Seatbelt early-exit certain cases.
**Sandbox is one dimension of the test matrix, not an optional plugin.**

### Point 15.6 · Windows

`codex-rs/core/src/windows_sandbox.rs`: elevated setup, private desktop, mode persistence, metrics. Non-Windows targets bail directly.

### Point 15.7 · bwrap (Linux filesystem sandbox main implementation)

`codex-rs/sandboxing/src/bwrap.rs`: bubblewrap is the main implementation of Linux **filesystem** sandbox (Landlock FS ruleset has retreated to legacy/fallback). `bwrap.rs:24` warns when system bwrap lacks user namespace — only in the few environments that cannot walk bubblewrap might it fall back to the Landlock FS path. **Reading order should be swapped with 15.4**: look at bwrap first, then Landlock’s “not enabled” comments, avoiding the mistaken belief that Landlock is carrying FS isolation. **(Verification correction 2026-08-04)**

### Point 15.8 · Orthogonal to approval (emphasized again)

| Layer | Question |
|---|---|
| AskForApproval | does the human agree |
| SandboxType | does the kernel allow |

Both must pass; then the side effect happens.

---

<h2 id="ch16">Chapter 16 · AskForApproval and approval cache</h2>

### Point 16.1 · Four-gear semantics (`codex-rs/protocol/src/protocol.rs:908-931`)

| Gear | One sentence |
|---|---|
| UnlessTrusted / untrusted | only let known-safe read-only through; everything else ask a human |
| OnRequest (default) | the model decides when to request |
| Granular | allow/deny by class; can forbid the dialog but it becomes Forbidden |
| Never | do not ask; failure returns to the model (CI-friendly) |

### Point 16.2 · Approval cache with_cached_approval (71–117)

- key is serializable
- skip only when all are `ApprovedForSession`
- after approve, write by key; subset requests can reuse

### Point 16.3 · Network approval

The orchestrator also hangs `begin_network_approval` / deferred mode — network is not a gift of the FS sandbox; it is another approval belt.

### Point 16.4 · Scenario scripts

| Scenario | Suggested combination |
|---|---|
| Interactive daily | OnRequest + platform default sandbox |
| CI | Never + Restricted FS + no network or proxy |
| High sensitivity | UnlessTrusted + denied-read list |
| Automated but need audit | Granular open critical classes, close dangerous classes |

---

<h2 id="ch17">Chapter 17 · AGENTS.md discovery algorithm line by line</h2>

### Point 17.1 · Constants and separators (`codex-rs/core/src/agents_md.rs:38-44`)

- `DEFAULT_AGENTS_MD_FILENAME = "AGENTS.md"`
- `LOCAL_AGENTS_MD_FILENAME = "AGENTS.override.md"`
- Between user instructions and project docs: `\n\n--- project-doc ---\n\n`

### Point 17.2 · load_project_instructions (53–81)

For each turn environment:

1. take `ExecutorFileSystem` (can be remote)
2. `read_agents_md`
3. errors only log; do not destroy the whole load
4. Some only if non-empty

**③** Multiple environments (local + remote exec-server) can each have their own instructions.

### Point 17.3 · Byte budget: a chain that is **first-come-first-served, and can starve later ones**

This is the mechanism in the whole chapter easiest to miss, with the most real consequences. State the default first (`codex-rs/config/src/config_toml.rs:68`):

```rust
pub const DEFAULT_PROJECT_DOC_MAX_BYTES: usize = 32 * 1024;   // 32 KiB
```

`codex-rs/core/src/config/mod.rs:207` aliases it as `AGENTS_MD_MAX_BYTES`; `:4071` is the fallback. `0` disables the whole feature (`agents_md.rs:95-99`).

**The key is that this 32 KiB is not “a cap per file,” but one budget shared by the whole chain** (`agents_md.rs:107-143`):

```python
remaining = max_total                      # :107
for p in paths:                            # paths are already root → cwd order
    if remaining == 0: break               # :111-113  budget exhausted; later files are not even read
    data = read(p)
    if size > remaining:
        data.truncate(remaining)           # :122-124  the file that crosses the budget is 【cut in half】
        warn("project doc exceeds remaining budget; truncating")
    text = utf8_lossy(data)
    if text.strip():                       # :133  blank files do not count
        entries.push(...)
        remaining -= len(data)             # :144  only non-empty content deducts budget
```

**Three non-intuitive consequences**:

| Behavior | Consequence |
|---|---|
| `remaining` decreases along **root → cwd** | **repo-root AGENTS.md eats the budget first**; deeper directories are more likely to get none |
| the file that crosses the budget is `truncate`d, not skipped | you get a **half instruction manual** — and it breaks in the middle, possibly cutting off the most important rule |
| deduction is written inside `if !text.trim().is_empty()` | **blank / pure-empty-line AGENTS.md does not consume budget**; leaving it around does not hurt |

> ⚠️ **Practical trap**: a monorepo, root `AGENTS.md` writes 30 KB of general norms, `apps/api/AGENTS.md` writes 5 KB of service-specific rules —
> **the latter is only read in 2 KB; remaining 3 KB silently lost**, logs only have one `warn`.
> You will think “why doesn’t the model follow our service’s rules,” but the problem is not the model.
>
> **④ Check**:
> - [ ] Is your root AGENTS.md already close to 32 KiB?
> - [ ] Are the more specific, more-should-win rules exactly at the end of the chain (the position most easily truncated)?
> - [ ] When something goes wrong, first look for the `project doc exceeds remaining budget; truncating` warn

### Point 17.3b · Who wins in the same directory: candidate_filenames

`agents_md.rs:234-248` decides **each directory** which filenames to try, in what order:

```rust
names.push(LOCAL_AGENTS_MD_FILENAME);      // "AGENTS.override.md"   ← first
names.push(DEFAULT_AGENTS_MD_FILENAME);    // "AGENTS.md"
for candidate in &config.project_doc_fallback_filenames {   // user-configurable fallback names
    if !candidate.is_empty() && !names.contains(&candidate) { names.push(candidate); }
}
```

On probe `return Ok(Some(candidate))` — **each directory takes only the first hit, no stacking** (`:220-227`).

So the accurate semantics of “override” is: **inside the same directory `AGENTS.override.md` makes `AGENTS.md` completely ineffective** (not merge, not append), but **does not affect other directories** — other layers on the chain still participate as usual.

> 📌 This differs from Claude Code’s `CLAUDE.local.md` mental model: there it is multi-source stacking; here it is **single-choice per layer + concatenation across layers**.

### Point 17.3c · The exit has two text suites: legacy vs environment-labeled

`LoadedAgentsMd` stores **a provenance-bearing entry list** (`entries: Vec<InstructionEntry>`, each with `InstructionProvenance`), not one concatenated string. Real assembly is in `text()` (`:310-316`):

```rust
pub fn text(&self) -> String {
    if self.has_multiple_project_environments() { self.environment_labeled_text() }
    else { self.legacy_text() }
}
```

**When multiple environments (local + remote exec-server both present) it switches to the environment-labeled version** — because then “which machine does this rule belong to” becomes information that must be expressed. Single environment walks legacy.

**The separator appears in only one place** (`:333-337`, the comment is very blunt):

```rust
// The project-doc marker tells the model where workspace-scoped
// instructions begin, so it is only needed on the transition
// from user or internal instructions to project instructions.
let separator = if is_project && !previous_was_project { AGENTS_MD_SEPARATOR } else { "\n\n" };
```

> 🔑 **Do not remember this wrong**: `--- project-doc ---` is **not the separator between every AGENTS.md on the chain**.
> It appears **once** — at the junction where “user/internal instructions” switch to “project instructions.”
> Between two adjacent project docs on the chain, ordinary `\n\n` is used.
>
> **③ Why**: this marker is a **semantic landmark** for the model (“from here on are this workspace’s rules”), not a typesetting divider. Putting it too many times dilutes its signal.

### Point 17.4 · agents_md_paths (155–220) step by step

1. Merge non-Project-layer config, parse `project_root_markers` (default `.git`)
2. `find_nearest_ancestor_with_markers` finds the project root
3. Walk from cwd to root; dirs list **reverse**s into root→cwd
4. If no root, search only cwd
5. For each directory, probe files by `candidate_filenames` (override-priority logic is in the candidate list)
6. Concurrent probe cap `MAX_CONCURRENT_ANCESTOR_PROBES = 256` (49)

### Point 17.5 · Contrast with Claude Code CLAUDE.md

| | CLAUDE.md | AGENTS.md (Codex) |
|---|---|---|
| Direction | multi-source stack (managed/user/project…) | root→cwd chain + user instructions |
| Override | @imports etc. | `AGENTS.override.md` |
| Root judgment | product rules | `project_root_markers` configurable |
| Remote FS | depends on implementation | first-class (ExecutorFileSystem) |

### Point 17.6 · Dogfood recursion

openai/codex’s own root `AGENTS.md` constrains “the agent changing this repo” — Seatbelt env vars, do not touch sandbox constants, core-bloat policy, the six context constitutions.
**The product loader and the engineering constitution share a name and a shape.**

```mermaid
flowchart TB
  CWD["cwd"] --> UP["walk up looking for markers"]
  UP --> ROOT["project root"]
  ROOT --> DOWN["root→cwd directory list"]
  DOWN --> F["each layer AGENTS.md / override"]
  F --> CAT["concat + byte cap"]
  CAT --> CTX["inject into model context"]
```

---

<h2 id="ch18">Chapter 18 · Skills · Plugins · Hooks · sub-agents</h2>

### Point 18.1 · When Skills / Plugins enter the turn

`run_turn` `build_skills_and_plugins` before sampling (defined @720, called @220). Failure → whole turn `Ok(None)`.
`prepare_tool_recommendations` / `built_tools` then handle tool_suggest and connector merge.

### Point 18.2 · Hooks lifecycle

| Timing | Function clue |
|---|---|
| Session start | `run_pending_session_start_hooks` |
| Record user input | `run_hooks_and_record_inputs` |
| Pre tool | registry `run_pre_tool_use_hooks` |
| Post tool | `run_post_tool_use_hooks` |
| Turn stop | `run_turn_stop_hooks` (can block continue-run) |
| Legacy after agent | `run_legacy_after_agent_hook` |

A Stop hook can **block ending and inject a continue-run prompt** (Chapter 7) — hooks are a control plane, not logs.

### Point 18.3 · MCP naming: prefix is legacy, dedup relies on SHA-1

This is much more complex than “add a prefix”; worth spreading out.

**① The prefix is not unconditional** (`codex-rs/codex-mcp/src/tools.rs:22`):

```rust
const LEGACY_MCP_TOOL_NAME_PREFIX: &str = "mcp__";
```

Note **LEGACY** in the constant name. Whether to add is decided by `Config::prefix_mcp_tool_names()` (`codex-rs/core/src/config/mod.rs:1756-1759`):

```rust
pub(crate) fn prefix_mcp_tool_names(&self) -> bool {
    !self.features.enabled(Feature::NonPrefixedMcpToolNames)
        || self.non_prefixed_mcp_tool_servers.is_some()
}
```

Reading: **prefix by default**; to remove it you must turn on `Feature::NonPrefixedMcpToolNames`; but as soon as `non_prefixed_mcp_tool_servers` is configured (per-server exemption list), it **returns to global prefix + exemption inside the list** (`tools.rs:141`).

**② Names the model sees have a hard constraint** (doc comment at `tools.rs:107-109`):

> Raw MCP server/tool names are kept on each `ToolInfo` for protocol calls, while `callable_namespace` / `callable_name` are **sanitized and, when necessary, hashed** so every model-visible name is **unique and <= 64 bytes**.

`MAX_TOOL_NAME_LENGTH = 64` (`:226`).

**sanitize itself is rougher than imagined** (`codex-rs/codex-mcp/src/mcp/mod.rs:450-465`):

```rust
for c in name.chars() {
    if c.is_ascii_alphanumeric() || c == '_' { sanitized.push(c); }
    else { sanitized.push('_'); }
}
if sanitized.is_empty() { "_".to_string() } else { sanitized }
```

The keep-set is only **`[A-Za-z0-9_]`**. Two consequences worth remembering:

| Input | Result |
|---|---|
| `repo-tools` (hyphen) | `repo_tools` — **even `-` is not kept** |
| `仓库管理` (Chinese) | `____` — **each Chinese character becomes one underscore** |
| all illegal characters | `"_"` |

> ⚠️ **Chinese-named MCP servers will go wrong**: `仓库管理` and `部署运维` are both four characters; after sanitize they **become exactly the same `____`**, judged as a name collision, each hung with a hash suffix; the model finally sees something like `mcp_______f61079/____` — **readability is completely lost**, and tool-selection quality drops with it.
>
> **④ Check**: when connecting MCP servers, use ASCII for server / namespace / tool names, and do not use hyphens.

**③ Dedup is two rounds, and uses a content hash not a serial number** (`:153-195`):

| Round | What it detects | On collision |
|---|---|---|
| First | the same `callable_namespace` mapping to **multiple different original identities** | `append_namespace_hash_suffix` — append a hash suffix to the namespace |
| Second | the same `(namespace, name)` corresponding to **multiple original identities** | `append_hash_suffix` — append a hash suffix to the name |

The hash is **SHA-1 taking the first several digits** (`:237-246`):

```rust
let mut hasher = Sha1::new();
hasher.update(s.as_bytes());
...
format!("_{}", &hash[..CALLABLE_NAME_HASH_LEN])
```

**④ Finally sort by `raw_tool_identity`** (`:196`) then produce.

> 🔑 **Why hash rather than `_1` `_2` serials**: serials depend on **traversal order** — today serverA connects first and is called `foo`; tomorrow serverB connects first and *it* is called `foo`; the tool name the model sees drifts between two sessions, **destroying prompt cache and the model’s tool memory**.
> The hash is decided by **content** (`raw_namespace_identity` concatenated from `server_name \0 callable_namespace \0 connector_id`); **the same tool is always the same name**, independent of connection order. The explicit `sort_by(raw_tool_identity)` at the end is a second insurance of the same motive.
>
> **This design principle can be carried away directly**: *for any identifier that must be stably exposed to the model, the dedup suffix must be decided by content, not by order.*
>
> **④ Check**: when you see a `_a1b2c3` at the tail of a tool name, do not think it is random — that is evidence of a name collision, meaning you connected two same-named servers/tools.

### Point 18.4 · Sub-agent registry (`codex-rs/core/src/agent/registry.rs`)

- limit same-session sub-thread count
- nickname + “the 2nd” suffix
- `exceeds_thread_spawn_depth_limit`
- SessionSource::SubAgent carries depth

**④ Check**: is recursive spawn stuck by depth? On nickname conflict, is it readable?

### Point 18.5 · ModeKind (`codex-rs/protocol/src/config_types.rs:630`)

**Easy to misread; say it clearly: the enum has 4 variants, but only 2 are externally visible.**

```rust
pub enum ModeKind {
    Plan,
    #[default]
    #[serde(alias = "code", alias = "pair_programming",
            alias = "execute", alias = "custom")]
    Default,
    #[doc(hidden)] #[serde(skip_serializing, skip_deserializing)]
    PairProgramming,          // hidden variant
    #[doc(hidden)] #[serde(skip_serializing, skip_deserializing)]
    Execute,                  // hidden variant
}
```

| You write in config | Which variant it actually lands on |
|---|---|
| `plan` | `Plan` |
| `default` / **`code`** / **`pair_programming`** / **`execute`** / **`custom`** | **all land on `Default`** |
| — | `PairProgramming` / `Execute` cannot be reached from config (`skip_deserializing`) |

**Key distinction**: `code`, `execute` etc. are **serde aliases hung on `Default`**, not independent modes.
And `PairProgramming` / `Execute` are **real variants that exist but are hidden** — they `skip_serializing` + `skip_deserializing` + `schemars(skip)` + `ts(skip)`, i.e. not into JSON, not into JSON Schema, not into TS type definitions.

**“There are only two modes” is nailed by one constant** (`:653`):

```rust
pub const TUI_VISIBLE_COLLABORATION_MODES: [ModeKind; 2] = [ModeKind::Default, ModeKind::Plan];
```

**③ Why this**: keep historically existing modes as variants (old rollout deserialization; internal code paths may still reference), but **simultaneously cut all their external surfaces**. This is a standard gradual-deprecation technique — safer than deleting the variants outright, cleaner than leaving them so users can configure them.

**④ Check**: when you see `mode = "execute"`, do not think you entered the `Execute` variant — you entered `Default`.

---

<h2 id="ch19">Chapter 19 · TUI · Exec · Doctor</h2>

### Point 19.1 · TUI is the default face

No subcommand enters TUI. `AGENTS.md` on center files such as `chatwidget.rs`: **add few methods, please split modules, target <500 LoC**.
Read TUI by “orchestration file vs feature module”; do not look for business truth in a 1400-line app.rs — business is in core.

### Point 19.2 · Exec non-interactive

`codex exec` → `codex_exec::Cli`. Typical pairing: `AskForApproval::Never` + tight sandbox.
Docs `docs/exec.md` link out to the official site.

### Point 19.3 · Doctor

Diagnose install, config, auth, runtime. When something is wrong, `codex doctor` first, then read source.

### Point 19.4 · Debug subcommands

`debug models` / `debug prompt-input` / app-server messages — print “what the model can see”; the debug exit of context discipline.

---

<h2 id="ch20">Chapter 20 · App-server · MCP · Rollout</h2>

### Point 20.1 · App-server = a socket for editors

A cluster of crates: `app-server`, `protocol`, `daemon`, `client`, `transport`…
CLI: `app-server`, `remote-control`, platform `app`.
Root `AGENTS.md` has a dedicated section on app-server API best practices — **breaking changes must search these surfaces**.

### Point 20.2 · MCP dual role

| Role | Entry |
|---|---|
| Client | `codex mcp` manages external servers |
| Server | `mcp-server` stdio exposes itself |

Inside a turn, lazy-wake required servers (Chapter 6).

### Point 20.3 · Rollout session assets (`codex-rs/rollout/src/lib.rs`)

- Directories: `sessions` / `archived_sessions`
- Compact, materialize references, search snippets, state_db
- Interaction sources: Cli, VSCode, custom atlas/chatgpt
- CLI: `resume`/`fork`/`archive`/`delete`

**③** A session is a first-class filesystem asset; backup/fork/audit are all built on this API.

### Point 20.4 · Contrast with OpenCode’s “the server is the body”

| | OpenCode | Codex |
|---|---|---|
| Body metaphor | HTTP server | core library + multiple frontends |
| IDE | client connects to server | app-server protocol cluster |
| Local default | TUI nested in server | TUI drives core directly |

---

<h2 id="ch21">Chapter 21 · Auth and model channel</h2>

### Point 21.1 · Login three-piece set (`login/`)

| Method | Clue |
|---|---|
| ChatGPT OAuth/PKCE | server.rs / pkce.rs |
| Device code | `device_code_auth.rs` (`run_device_code_login` @234) |
| API Key | CLI `run_login_with_api_key`; env telemetry distinguishes OPENAI_API_KEY / CODEX_API_KEY |

### Point 21.2 · Commercial closed loop

Recommend ChatGPT plan login — symmetric with Claude Code’s “subscription” narrative: the CLI is the local surface of plan entitlements.

### Point 21.3 · Models and tool protocol

Sampling walks Responses API shape (ToolSpec comments chain to platform.openai.com).
The workspace contains `ollama`, `lmstudio` and other crates — **local/OSS paths exist**; default routing treats runtime config / `codex debug models` as canonical; this lecture does not nail easily-changing default slugs.

### Point 21.4 · Auth premise of previous-model compact

Chapter 9: only Codex backend + OpenAI provider does previous-model compatibility compact.
When switching to a third-party provider, do not expect this behavior.

---

<h2 id="ch22">Chapter 22 · Distinctive designs, trade-offs, how to read, comparisons</h2>

### Point 22.1 · Three most distinctive designs (expanded)

**A. OS sandbox as a first-class citizen**
Not a prompt writing “please be careful,” but Seatbelt/Landlock/Token + when denied-read conflicts with raise-privilege, keep the deny.

**B. Approval × sandbox × execpolicy three-layer synthesis**
Orchestrator synthesizes human review, kernel, and command policy into `SandboxAttempt`; Granular turning off the dialog → Forbidden to prevent misunderstanding.

**C. Context constitution into the repo**
The six rules + Fragment type + `debug prompt-input` (the real command is `run_debug_prompt_input_command` → `codex_core::build_prompt_input`; there is no `debug_input` identifier in the whole repo — verification correction 2026-08-04).

### Point 22.2 · Three trade-offs you must know

1. **core/turn is still huge** — policy resists bloat; historical debt remains; the way to read is “jump by function,” not linear through-read.
2. **User docs are external** — `docs/` often jumps out; offline you rely only on source and this analysis.
3. **Tool surface leans shell+patch** — aligned with some model distributions; fine-grained navigation teaching is less friendly than CC’s illustrated catalog; you have to rely on the sandbox to backstop an over-wide shell.

### Point 22.3 · Suggested reading order (deepened edition)

1. `cli/src/main.rs` subcommand map (30 minutes)
2. `codex-rs/core/src/tasks/regular.rs` full text (15 minutes)
3. `codex-rs/core/src/session/turn.rs:153-520` entry+stop (2 hours) — note the repo has 3 same-named `turn.rs`; the real one is `core/src/session/`
4. `turn.rs:1308-1405` + `2165-2260` sampling shell (1 hour)
5. `orchestrator.rs` header comments + `codex-rs/core/src/tools/sandboxing.rs:156-300` (2 hours)
6. `handlers/shell.rs:63-220` (1 hour)
7. `agents_md.rs:1-220` (1 hour)
8. `manager.rs` SandboxType + `landlock.rs` rule install (1 hour)
9. Contrast *Claude-Code-vs-OpenCode-vs-Codex deep comparison*

### Point 22.4 · Selection, bony

```mermaid
flowchart TB
  Q["local coding Agent"] --> A["closed-source extreme experience"]
  Q --> B["open-source multi-model kitchen"]
  Q --> C["open-source official + OS sandbox"]
  A --> CC["Claude Code"]
  B --> OC["OpenCode"]
  C --> CX["Codex CLI"]
```

### Point 22.5 · Unknown inventory (still honest after deepening)

| Item | Status |
|---|---|
| Seatbelt profile generation details | need to continue drilling macOS-specific modules (this edition did not split the plist line by line) |
| App-server full RPC surface | boundary set; method table not fully recorded |
| Default model slug | volatile; use debug models |
| Cloud tasks protocol | marked experimental; not dug deep |
| memories read/write full product semantics | crates exist; not expanded as a main line |

---

## Appendix A · “One point, one page” index (jump-read by topic)

| You want to understand | Go to chapter |
|---|---|
| Two-layer loop | Chapters 5–7 |
| Why it will not stop / suddenly stopped | Chapter 7 stop table |
| Stream retry | Chapter 8 |
| When compact happens | Chapter 9 |
| Can you change history | Chapter 10 |
| Where tools come from | Chapter 11 |
| Approval and sandbox, who first | Chapter 12 |
| Shell raise-privilege rejected | Chapter 13 |
| Multi-file patch asked only once | Chapters 14, 16 |
| Which directories Linux can write | Chapter 15 |
| Why AGENTS.md was not read | Chapter 17 |
| Stop hook continue-run | Chapters 7, 18 |
| How to back up a session | Chapter 20 |
| How to configure CI | Section 16.4 |

## Appendix B · Source-path quick lookup

| Topic | Path |
|---|---|
| CLI | `codex-rs/cli/src/main.rs` |
| RegularTask | `codex-rs/core/src/tasks/regular.rs` |
| run_turn | `codex-rs/core/src/session/turn.rs:153` |
| run_sampling_request | same file `:1308` |
| try_run_sampling_request | same file `:2165` |
| Orchestrator | `codex-rs/core/src/tools/orchestrator.rs` |
| Approval/sandbox traits | `codex-rs/core/src/tools/sandboxing.rs` |
| Shell | `codex-rs/core/src/tools/handlers/shell.rs` |
| ApplyPatch handler | `.../handlers/apply_patch.rs` |
| ToolSpec | `codex-rs/tools/src/tool_spec.rs` |
| AskForApproval | `codex-rs/protocol/src/protocol.rs:908` |
| SandboxType | `codex-rs/sandboxing/src/manager.rs:35` |
| Landlock | `codex-rs/linux-sandbox/src/landlock.rs` |
| AGENTS.md | `codex-rs/core/src/agents_md.rs` |
| arg0 | `codex-rs/arg0/src/lib.rs` |
| Rollout | `codex-rs/rollout/src/lib.rs` |
| Constitution | `AGENTS.md` |

## Appendix C · Scale snapshot (bb1af23)

| Item | Value |
|---|---|
| workspace members | 126 |
| `.rs` files | 2783 |
| turn.rs | 2741 |
| session/mod.rs | 4154 |
| orchestrator.rs | 533 |
| apply_patch handler | 666 |
| agents_md.rs | 480 |
| sandboxing manager | 715 |

---



---

# Part VII · Scenario-level step-by-step walkthroughs

> Earlier we split points by subsystem; here we string the points into a timeline by **real user action**. Every step points back to a chapter and line numbers.

<h2 id="ch23">Chapter 23 · Scenario: executing `rm -rf build` in an interactive session</h2>

### Story

You say in TUI: “delete the build directory.” The model decides to call shell.

### Step-by-step walkthrough

| Step | What happens | Back-pointer |
|---|---|---|
| 1 | no subcommand, enter TUI, submit Op | Chapters 3, 4 |
| 2 | RegularTask emits TurnStarted, calls run_turn | Chapter 5 |
| 3 | pre compact / MCP / step_context / skills | Chapter 6 |
| 4 | run_sampling_request → stream → FunctionCall(shell) | Chapter 8 |
| 5 | Shell `run_exec_like`: merge permissions; if not raise-privilege, pass the guard | Chapter 13 `shell.rs:122-138` |
| 6 | `create_exec_approval_requirement_for_command` | Chapter 13 |
| 7 | Orchestrator: if NeedsApproval → dialog; cache ApprovedForSession | Chapters 12, 16 |
| 8 | select SandboxType (e.g. macOS Seatbelt) | Chapter 15 |
| 9 | SandboxAttempt executes; if sandbox denies and can escalate → retry **without asking a human again** | orchestrator header comment |
| 10 | output back to model; needs_follow_up? may sample again to confirm | Chapter 7 |

### Key forks

**A. Policy = Never (misconfigured in interactive)**
Do not ask a human; try directly; sandbox may still block. Failure returns to the model — the user may see the model “try failed” rather than a dialog.

**B. Policy = UnlessTrusted**
`rm` almost always asks.

**C. Request escalated + not OnRequest**
`shell.rs:122-138` **Reject directly**; copy educates the model not to ask for raise-privilege.

**D. A denied-read path exists**
Even if raise-privilege is approved, `sandbox_permissions_preserving_denied_reads` may force continuing the sandbox (Section 12.4).

### Reader check

- [ ] Can you point out whether “ask a human” happens in the orchestrator or the shell front guard? (both places have it)
- [ ] After sandbox deny, will the escalate still pop a dialog? (by design: no re-review)

---

<h2 id="ch24">Chapter 24 · Scenario: a multi-file apply_patch</h2>

### Story

The model’s one patch changes `src/a.ts` and `src/b.ts`.

### Step-by-step walkthrough

1. The tool can be the `apply_patch` handler, or shell text intercepted by `intercept_apply_patch` (Chapters 13.2, 14).
2. `Approvable::approval_keys` returns **two path keys** (sandboxing comments 316–321).
3. `with_cached_approval`: skip only when **both keys are already ApprovedForSession**; after approve, write each.
4. Next time only change `a.ts` → single key hits cache → may not ask.
5. Landing on disk walks the apply-patch crate parsing Hunk.

### Why this is better than “one boolean for the whole tool call”

| Scheme | Problem |
|---|---|
| Whole-call approve | next time changing an unrelated file also skips asking; too wide |
| Per-file key | granularity aligns with real risk; subsets reusable |

### Contrast Claude Code

CC’s Edit tool is usually single-file semantics; Codex explicitly writes “multi-file one patch” into the approval-cache contract.

---

<h2 id="ch25">Chapter 25 · Scenario: mid-turn compact when the context window is about to blow</h2>

### Story

In a long session the model still needs to call tools, but token_status shows it hit the top.

### Step-by-step walkthrough

1. sampling returns `model_needs_follow_up=true` (7.2).
2. `needs_follow_up` is true; `token_limit_reached` or `new_context_window_request` → `should_roll_over` (419–420).
3. `run_auto_compact(..., MidTurn, BeforeLastUserMessage{world_state, step_context})` (431–443).
4. If success: may run session start hooks again; `can_drain_pending_input = !model_needs_follow_up`; `continue` back to the loop.
5. Comment 430: believe compact can pull the window back; accept potential extra laps.

### Difference from pre-turn compact

| | PreTurn | MidTurn |
|---|---|---|
| Timing | before sampling | after sampling when still continuing |
| Input | this round not yet the focus | must keep last user message’s relative position |
| Injection | DoNotInject etc. | BeforeLastUserMessage |

### Failure

Compact throws non-Abort → emit turn error → `Ok(None)` (449–452). User side appears as a turn error-end, not silently dropping history.

---

<h2 id="ch26">Chapter 26 · Scenario: a stop hook demands continue-running</h2>

### Story

The model seems finished speaking (no tool follow-up), but a Stop hook is installed to check “did tests pass.”

### Step-by-step walkthrough

1. `!needs_follow_up` enters the stop protocol (461+).
2. `run_turn_stop_hooks` → `should_block=true` and carries continuation fragments.
3. `build_hook_prompt_message` succeeds → record into conversation, accept mailbox, `stop_hook_active=true`, `continue` (470–486).
4. Next-lap sampling treats the hook prompt as context; may run test commands again.
5. If block but no prompt → Warning “ignoring the block” (488–494).

### Product meaning

**Hooks can veto “the model thought it was done”** — this is an enterprise compliance/quality-gate hook, not UI decoration.

### Contrast Open Design Design Jury

OD uses multi-round review inside a session; Codex stop hook is a **locally scriptable latch**. Similar direction (do not let it clock out easily), different mechanism.

---

<h2 id="ch27">Chapter 27 · Scenario: `codex exec` + Never in CI</h2>

### Recommended combination

- `codex exec "..."`
- `AskForApproval::Never`
- FS Restricted + network restricted or proxy
- no TUI

### Stepwise differences (vs interactive)

| Point | Interactive | CI |
|---|---|---|
| Facade | TUI | exec |
| Approval | OnRequest common | Never |
| Failure | can ask a human to escalate | return to model or fail-exit directly |
| Session | resume/fork assets | often one-shot rollout |

### Risk

Never is **not** “skip the sandbox.” If someone configures SandboxType as None + DangerFullAccess, CI becomes a true bare run — when reviewing config, look at both layers.

---

<h2 id="ch28">Chapter 28 · Scenario: AGENTS.md chain vs override — who wins</h2>

### Story

Repo root has `AGENTS.md`; `apps/api/AGENTS.override.md` exists; cwd is `apps/api`.

### Step-by-step walkthrough

1. markers find git root.
2. search_dirs = `[root, ..., apps/api]` (root→cwd).
3. each layer candidate_filenames: override preferred over default (Chapter 17).
4. `apps/api` layer, if it reads the override file, uses it (same layer).
5. concat is cut by `project_doc_max_bytes`.
6. joined to user instructions with `--- project-doc ---`.

### Common “not read” cause list

- [ ] `project_doc_max_bytes = 0`
- [ ] cwd is outside root and there are no markers
- [ ] filename is neither AGENTS.md nor override / fallback
- [ ] remote environment’s FS cannot see the local path
- [ ] content all blank, skipped

---

## Appendix D · Same question, different answers: Claude Code / OpenCode / Codex detailed table

| Question | Claude Code | OpenCode | Codex (this lecture) |
|---|---|---|---|
| User interjection | product queue | depends on implementation | RegularTask outer + input_queue |
| Main-loop file | deep edition Ch.6 | prompt.ts | turn.rs run_turn |
| Stop | full set of termination reasons | Goal/doom etc. (MiMo strengthened) | follow-up × compact × stop hook |
| Patch | Edit/Write | edit or apply_patch | apply_patch + intercept |
| Permissions | five gates | ruleset | AskForApproval × ExecApprovalRequirement × Sandbox |
| Sandbox | mainly product layer | rules/optional | OS-level three platforms |
| Instructions | CLAUDE.md | memory/config | AGENTS.md chain |
| Session | product storage | DB/files | rollout file assets |
| Open source | no | MIT | Apache-2.0 |

---

## Appendix E · Glossary (for reading source)

| Term | Meaning |
|---|---|
| Turn | a user-triggered task unit; may contain multiple samplings |
| StepContext | a single-step frozen tool/config/world view |
| SamplingRequest | one model streaming request |
| ToolOrchestrator | approval+sandbox+retry hub |
| SandboxAttempt | one typed execution attempt |
| Rollout | session land-on-disk and replay assets |
| arg0 personality | same binary dispatched by argv0 |
| comp_hash | compact-compatibility hash |


## Closing

The deepened edition takes Codex from “yet another Agent” apart into a **control system you can check point by point**: outer interjection, inner tools, compact opening a window, stop hook, approval three states, sandbox raise-privilege conflict, AGENTS.md hard budget.
What you want is not more adjectives, but **every point can point to a line number, state a trade-off, list a checklist** — this lecture was rewritten to that standard.

---

*This lecture was written by checking point by point against `openai/codex` commit `bb1af235ea2822d7a40f75ef52e4d6a2cde84da2` (2026-07-28). All `file:line` citations were re-verified with `grep -n` / `sed -n`; ambiguous filenames (`turn.rs` / `shell.rs` / `protocol.rs` / `config_types.rs` each have 3–6 same-named files in this repo) always have the crate path filled in. Statistics come from a local full checkout (2,783 `.rs` under `codex-rs`, workspace 126 members); community data from the GitHub REST API on 2026-07-29 (102,170★ / 15,331 fork).*

**Contrast reading** → [three-way deep comparison](Claude-Code-vs-OpenCode-vs-Codex-深度对比.md) · [interactive explainer](../openai-codex-解析.html) (5 runnable benches) · [catalog](../总目录.md) · [sideways comparison and overall evaluation](横向对比与总结评价.md)
