Surface the offered tool + skill/slash-command catalog in the `--json` event stream
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_searchwhen 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_executionwhose shell command opensskills/<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:
--jsononly; 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/skillsout-of-band - re-implements Codex's own discovery (plugins, profiles,-coverrides, MCP negotiation) and drifts from what the session actually loaded.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗