Surface the offered tool + skill/slash-command catalog in the `--json` event stream

Open 💬 3 comments Opened Jul 4, 2026 by geserdugarov

What variant of Codex are you using?

CLI

What feature would you like to see?

At the start of a codex exec --json run (and the equivalent app-server thread stream), emit a first-class event that declares the catalog offered to the model for that session, before the first turn:

  • tools — the tool surface the model can call this session: the built-in shell / apply_patch, web_search when enabled, and every configured MCP tool (fully-qualified name, e.g. mcp__tavily__search).
  • skills / slash-commands - the skills discoverable this session ($CODEX_HOME/skills/**/SKILL.md, repo-local .agents/skills / .codex/skills, plugin-provided), and/or the available slash-commands.

Concretely, either extend thread.started or add a dedicated session.configured event carrying tools[] and skills[] arrays. This is the direct analogue of Claude Code's headless --output-format stream-json system/init frame, which already emits tools, slash_commands, and skills up front.

Why

Tools that wrap codex exec --json for observability/analytics cannot currently reconstruct what the model was offered - only what it used. From the stream you can observe:

  • tools used - via item.* events (command_execution, mcp_tool_call, web_search, file_change), and
  • skills triggered - only heuristically, by noticing a command_execution whose shell command opens skills/<name>/SKILL.md.

There is no way to answer the more useful questions:

  • Which tools/skills were available but never used?
  • Was a skill skipped because it wasn't discoverable, or because the model chose not to use it? (These are indistinguishable today.)
  • Which MCP tools actually made it into the session's tool set?

Claude Code answers all of these from its init frame, so a single dashboard can show "offered → triggered" for Claude runs but is blank for Codex runs of the same workflow. Codex already knows this catalog internally at session construction; it just never reaches the JSONL consumer. This is the same class of gap as #17501 (MCP startup notifications), one payload deeper.

Current behavior (what codex exec --json emits today)

Tested on codex-cli 0.142.5. A full, successful turn:

{"type":"thread.started","thread_id":"019f2b90-0c34-77a1-ad02-b38355965b93"}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"hi there"}}
{"type":"turn.completed","usage":{"input_tokens":5,"cached_input_tokens":0,"output_tokens":2,"reasoning_output_tokens":0}}

The complete event vocabulary is thread.started, turn.started, item.*, turn.completed, turn.failed, error. thread.started carries only thread_id; turn.started is empty. Nothing declares the offered tools or available skills.

For contrast, Claude Code's --output-format stream-json opens with:

{"type":"system","subtype":"init","session_id":"…","model":"…",
 "tools":["Bash","Read","Edit","Skill","WebSearch", …],
 "slash_commands":["review","init", …],
 "skills":["develop","review","deep-research","dataviz","verify", …]}

Proposed event shape

Option A - extend thread.started (backward-compatible; new optional fields):

{"type":"thread.started",
 "thread_id":"…",
 "model":"gpt-5-codex",
 "tools":["shell","apply_patch","web_search","mcp__tavily__search"],
 "skills":["develop","review"]}

Option B - a dedicated event emitted right after thread.started:

{"type":"session.configured",
 "thread_id":"…",
 "model":"gpt-5-codex",
 "tools":[{"name":"shell","source":"builtin"},
          {"name":"apply_patch","source":"builtin"},
          {"name":"mcp__tavily__search","source":"mcp","server":"tavily"}],
 "skills":[{"name":"develop","source":"repo"},
           {"name":"review","source":"repo"}]}

Constraints:

  • --json only; no change to human-readable output or model behavior.
  • Additive/optional so existing parsers keep working.
  • Ideally mirrored in the app-server thread stream for parity.
  • Names only for skills (no need to stream SKILL.md contents).

Alternatives considered

  • Infer offered tools from used item.* events - only yields the used subset; can never show "available but unused," and misses tools in a run that happened to use none.
  • Parse the local config / $CODEX_HOME/skills out-of-band - re-implements Codex's own discovery (plugins, profiles, -c overrides, MCP negotiation) and drifts from what the session actually loaded.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗