Skip to main content
Reference Last updated: 8 October 2026

Codex exec --json event cheatsheet

Nathan Schram
By Founder, Little Bear Apps

codex exec --json writes one JSON object per line (JSONL) to stdout. Each line is a top-level thread event with a type field.

Below: required + commonly emitted fields for every line type plus a full-line example for each shape that can be emitted. Fields noted as optional may be omitted (or null) depending on Codex version and lifecycle. Unknown fields may appear; ignore what you don’t use.

Top-level event lines (non-item)

thread.started

Fields:

  • type
  • thread_id

Example:

{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}

turn.started

Fields:

  • type

Example:

{"type":"turn.started"}

turn.completed

Fields:

  • type
  • usage.input_tokens
  • usage.cached_input_tokens — a subset of input_tokens
  • usage.cache_write_input_tokens — added upstream in #33454 (defaults to 0)
  • usage.output_tokens
  • usage.reasoning_output_tokens — a subset of output_tokens (never add it on top)

usage is the thread’s running total, not this turn’s: Codex emits usage_from_last_total(), and exec resume seeds the total from the rollout, so a resumed run reports every earlier run on the thread + this run. A turn with no model call reports all zeros; a failed turn reports no usage (its tokens land in the next run’s total). Verified on 0.157.1 (#419 step-0 capture, tests/fixtures/codex_0157_resume_usage.jsonl): three runs on one thread reported input 10656 → 21328 → 32017.

Example (0.157.1):

{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"cache_write_input_tokens":0,"output_tokens":122,"reasoning_output_tokens":64}}

Per-run token attribution in Untether (#419). The runner forwards the raw thread total; the bridge records it in session_costs.json (key codex:<thread_id>) and reports usage(run N) − usage(run N−1) for that thread (per field, with a per-field-max baseline so a lower total never re-counts). The first run of a fresh thread is source=new_session; a resumed thread Untether has never seen (e.g. /continue of a thread started in a terminal) records the raw total once as source=baseline_unknown. Tokens spent on the thread outside Untether between two Untether runs (a terminal codex resume <id>, or a rollback window) are inside the next total, so they land in the next run’s delta as source=ledger — exact in aggregate, not per run. thread.started is re-emitted with the same thread_id on both exec resume <id> and exec resume --last (verified on 0.157.1), so reply resumes and /continue both reach the ledger.

turn.failed

Fields:

  • type
  • error.message

Example:

{"type":"turn.failed","error":{"message":"model response stream ended unexpectedly"}}

error

Fields:

  • type
  • message

Example:

{"type":"error","message":"stream error: broken pipe"}

Note: Codex may emit transient reconnect notices as type="error" with messages like "Reconnecting... 1/5" while it retries a dropped stream. Treat those as non-fatal progress updates (the turn continues).

Item event lines (item.*)

Every item line includes:

  • type (item.started, item.updated, or item.completed)
  • item.id
  • item.type
  • fields for the specific item.type below

item.id is stable for the item; updates/completion reuse the same id.

agent_message (only item.completed)

Fields:

  • item.text
  • item.phase (optional) — commentary for interim progress text, final_answer for the answer; absent on older versions

Example:

{"type":"item.completed","item":{"id":"item_3","type":"agent_message","text":"Done. I updated the docs and added examples."}}

reasoning (only item.completed, if enabled)

Fields:

  • item.text

Example:

{"type":"item.completed","item":{"id":"item_0","type":"reasoning","text":"**Scanning docs for exec JSON schema**"}}

command_execution (item.started and item.completed)

Fields:

  • item.command
  • item.aggregated_output
  • item.exit_code (null or omitted until completion)
  • item.status (in_progress, completed, failed, declined)

Example (started):

{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"","exit_code":null,"status":"in_progress"}}

Example (completed, success):

{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"bash -lc ls","aggregated_output":"docs\nsrc\n","exit_code":0,"status":"completed"}}

Example (completed, failure):

{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"bash -lc false","aggregated_output":"","exit_code":1,"status":"failed"}}

Note: aggregated_output is truncated to 64 KiB; truncated output ends with \n...(truncated).

file_change (only item.completed)

Fields:

  • item.changes[].path
  • item.changes[].kind (add, delete, update)
  • item.status (completed, failed)

Example:

{"type":"item.completed","item":{"id":"item_4","type":"file_change","changes":[{"path":"docs/exec-json-cheatsheet.md","kind":"add"},{"path":"docs/exec.md","kind":"update"}],"status":"completed"}}

mcp_tool_call (item.started and item.completed)

Fields:

  • item.server
  • item.tool
  • item.arguments (JSON value; defaults to null if absent)
  • item.result (object or null; may be omitted)
  • item.result.content (array of MCP content blocks)
  • item.result.structured_content (JSON value or null)
  • item.error (object or null; may be omitted)
  • item.error.message (if error is present)
  • item.status (in_progress, completed, failed)

Example (started):

{"type":"item.started","item":{"id":"item_5","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":null,"error":null,"status":"in_progress"}}

Example (completed, success):

{"type":"item.completed","item":{"id":"item_5","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":{"content":[{"type":"text","text":"Found 3 matches.","annotations":{"audience":["assistant"],"lastModified":"2025-01-01T00:00:00Z","priority":0.5}}],"structured_content":{"matches":3}},"error":null,"status":"completed"}}

Example (completed, failure):

{"type":"item.completed","item":{"id":"item_6","type":"mcp_tool_call","server":"docs","tool":"search","arguments":{"q":"exec --json"},"result":null,"error":{"message":"tool timeout"},"status":"failed"}}

web_search (item.started and item.completed)

Fields:

  • item.id — see the duplicate-id note below
  • item.query — "" on item.started; the real query (or "") on item.completed
  • item.action — tagged by type: search (query?, queries?) | open_page (url?) | find_in_page (url?, pattern?) | other. item.started always carries {"type":"other"}.
  • item.results (completed only, optional) — array of opaque result objects

Duplicate id key on the wire: upstream serialises ThreadItem { id, #[serde(flatten)] details } plus the item’s own id, so each line carries "id":"item_N" and then the raw id ("id":"exec-…"). Untether’s decoder (msgspec) keeps the last key — the raw id shared by started and completed — so the two phases pair. Pinned by test_web_search_duplicate_id_keeps_last.

Example (0.157.1 wire, trimmed):

{"type":"item.started","item":{"id":"item_2","type":"web_search","id":"exec-8c5b…","query":"","action":{"type":"other"}}}
{"type":"item.completed","item":{"id":"item_2","type":"web_search","id":"exec-8c5b…","query":"codex-cli latest version npm GitHub","action":{"type":"search","query":"codex-cli latest version npm GitHub"},"results":[{"type":"text_result","title":"@openai/codex - npm","url":"https://www.npmjs.com/package/%40openai/codex"}]}}

Legacy (≤ 0.125) lines carry only id + query and still decode.

collab_tool_call (sub-agent coordination)

Fields (all optional except item.id):

  • item.tool
  • item.sender_thread_id
  • item.receiver_thread_ids[]
  • item.prompt
  • item.agents_states (map of thread id → {status, message?})
  • item.status (in_progress, completed, failed)

Untether decodes it but does not render it.

todo_list (item.started, item.updated, and item.completed)

Fields:

  • item.items[].text
  • item.items[].completed

Example (started):

{"type":"item.started","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":false},{"text":"Write cheatsheet","completed":false}]}}

Example (updated):

{"type":"item.updated","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":true},{"text":"Write cheatsheet","completed":false}]}}

Example (completed):

{"type":"item.completed","item":{"id":"item_8","type":"todo_list","items":[{"text":"Scan docs","completed":true},{"text":"Write cheatsheet","completed":true}]}}

error (non-fatal warning as an item; only item.completed)

Fields:

  • item.message

Example:

{"type":"item.completed","item":{"id":"item_9","type":"error","message":"command output truncated"}}

Unknown item types

An item.* line with an item.type not listed here decodes as an unknown_item (id, type and remaining fields kept) rather than failing the line; Untether does not render it.

MCP content block shapes (mcp_tool_call.result.content)

result.content is an array of content blocks. Each block is one of the types below; all optional fields may appear depending on the server.

Text content

Fields:

  • type
  • text
  • annotations.audience (optional)
  • annotations.lastModified (optional)
  • annotations.priority (optional)

Example block:

{"type":"text","text":"Hello","annotations":{"audience":["assistant"],"lastModified":"2025-01-01T00:00:00Z","priority":0.5}}

Image content

Fields:

  • type
  • data (base64)
  • mimeType
  • annotations.* (same as above, optional)

Example block:

{"type":"image","data":"<base64>","mimeType":"image/png","annotations":{"audience":["assistant"]}}

Audio content

Fields:

  • type
  • data (base64)
  • mimeType
  • annotations.* (optional)

Example block:

{"type":"audio","data":"<base64>","mimeType":"audio/wav","annotations":{"audience":["assistant"]}}

Fields:

  • type
  • name
  • uri
  • description (optional)
  • mimeType (optional)
  • size (optional)
  • title (optional)
  • annotations.* (optional)

Example block:

{"type":"resource_link","name":"docs/exec.md","uri":"file:///repo/docs/exec.md","description":"Exec docs","mimeType":"text/markdown","size":1234,"title":"exec.md","annotations":{"audience":["assistant"]}}

Embedded resource

Fields:

  • type
  • resource (either text or blob contents)
  • annotations.* (optional)

Example block (embedded text):

{"type":"resource","resource":{"uri":"file:///repo/README.md","text":"Hello","mimeType":"text/markdown"},"annotations":{"audience":["assistant"]}}

Example block (embedded blob):

{"type":"resource","resource":{"uri":"file:///repo/image.png","blob":"<base64>","mimeType":"image/png"},"annotations":{"audience":["assistant"]}}

Consumer considerations (rendering + success/failure)

Use this section to decide what to surface to end users vs. what to treat as machine-only metadata.

What to render for users

  • Final answer: render item.completed where item.type = "agent_message" as the main response.
  • Progress updates (optional):
    • item.completed with item.type = "reasoning" can be shown as brief activity breadcrumbs (only if you want to expose reasoning summaries).
    • item.started / item.completed with item.type = "command_execution" can be shown as “running command …” status lines without printing full output.
    • item.completed with item.type = "file_change" can be rendered as a list of changed paths and kinds (add/update/delete).
    • item.* with item.type = "todo_list" can be shown as a progress checklist.
  • Errors: render type = "error" and item.type = "error" as user-visible warnings or failures.

Fields you can safely skip for UX

  • command_execution.aggregated_output is often noisy; many consumers omit or truncate it, and rely on command_execution.status + exit_code instead.
  • mcp_tool_call.result.content can be large and tool-specific; consider showing only high-level status unless you know the tool’s schema.
  • usage fields (turn.completed.usage.*) are the thread’s running total — subtract the previous run’s total for per-run figures (Untether does, #419).

Success and failure signals

  • Turn success: type = "turn.completed" indicates overall success.
  • Turn failure: type = "turn.failed" with error.message indicates failure.
  • Item success/failure: use item.status on the item payload:
    • command_execution.status: completed = success, failed = failure.
    • file_change.status: completed = patch applied, failed = patch failed.
    • mcp_tool_call.status: completed = tool succeeded, failed = tool failed.
  • Fatal stream errors: type = "error" means the JSONL stream itself hit an unrecoverable error (except transient "Reconnecting... X/Y" notices, which are non-fatal).

Suggested minimal rendering

If you want a compact UI, the following is usually enough:

  • Thread/turn lifecycle: thread.started, turn.started, turn.completed or turn.failed
  • Final answer: item.completed with item.type = "agent_message"
  • Optional progress: item.started / item.completed for command_execution and file_change

Optional/conditional emission notes

  • turn.failed only appears on failure; otherwise turn.completed is emitted.
  • reasoning items only appear when reasoning summaries are enabled.
  • todo_list items only appear when the plan tool is active; they are the primary source of item.updated.
  • file_change items are emitted only as item.completed in the current codex exec --json stream; web_search is emitted on item.started (empty query, action.type = "other") and item.completed since 0.157.x.

Not emitted: context window, per-request usage, compaction

codex exec --json carries no model context window, no per-request token usage (only the thread total on turn.completed, above) and no compaction event. So Untether can’t show the N% ctx status segment or the 🗜️ compaction row for Codex, which the Claude runner does (#819). Both are tracked in #832 (a rollout-file side-channel, or the app-server protocol).

See also

Was this helpful?

Related Articles