| name | openai-codex-sdk |
| description | Authoritative reference for working on MultiTable's OpenAI Codex SDK integration (`@openai/codex-sdk`). Trigger when the user mentions the Codex SDK, `Codex`, `Thread`, `runStreamed`, `ThreadEvent`, `ThreadItem`, sandbox/approval policy, `~/.codex/sessions`, `resumeThread`, `codex exec`, plan-mode for Codex, usage limits / rate limits (`account/rateLimits/updated`, `RateLimitSnapshot`), or modifying anything under `packages/daemon/src/agent/providers/codex.ts` or `packages/daemon/src/transcripts/codexParser.ts`. |
| allowed-tools | Read, Grep, Glob, Bash, Edit, Write |
OpenAI Codex SDK reference for MultiTable
Pinned SDK version: @openai/codex-sdk@0.128.0 (see node_modules/@openai/codex-sdk/package.json). Every type signature and behavior in this skill is quoted from the local dist/index.d.ts / dist/index.js so it matches what we have installed — re-verify after any SDK upgrade.
This skill is strictly Codex-only. Do not import Claude Agent SDK concepts (canUseTool, onElicitation, hooks, plan mode, AskUserQuestion) into Codex code or reasoning. Codex has none of those primitives. For Claude SDK work see claude-agent-sdk/SKILL.md.
The one fact that shapes everything
The Codex TypeScript SDK is a stateless wrapper around codex exec --experimental-json. Every runStreamed() / run() call:
- Spawns a fresh
codex child process.
- Writes the prompt to its stdin.
- Closes stdin immediately (no further input is possible).
- Reads JSONL events from stdout until EOF.
- Kills the child on completion / abort.
There is no long-lived child and no host-side approval callback. All approval / tool gating decisions are made in-process inside the spawned codex binary based on flags set at spawn time. This single fact is why approvalPolicy must be 'never', why there is no canUseTool, and why "intercepting a tool call before execution" is fundamentally not a thing in this SDK.
Quick task → file map
Decision tree: which lever to pull?
Need to BLOCK a tool call?
├── Block writes entirely? ─── ThreadOptions.sandboxMode: 'read-only'
├── Block network in workspace? ─── ThreadOptions.networkAccessEnabled: false
├── Block paths outside cwd? ─── do NOT add them to additionalDirectories
├── Block specific MCP tools? ─── CodexOptions.config.mcp_servers.<name>.disabled_tools / enabled_tools
└── Block reactively? ─── watch the event stream and ctrl.abort() on undesirable item.started
Need the agent to ASK the user a question?
└── NOT POSSIBLE in @openai/codex-sdk. There is no host-side prompt callback.
The user-visible TUI "/plan" → "approve plan" flow is a TUI feature, not an SDK primitive.
Need to RUN code on a lifecycle event?
└── NOT POSSIBLE. There are no hooks. Watch ThreadEvent stream from your for-await loop.
Need to STOP a turn mid-stream?
└── Pass an AbortController.signal as TurnOptions.signal to runStreamed/run.
The signal is forwarded to child_process.spawn; child gets SIGTERM.
Need to RESUME a prior conversation?
└── codex.resumeThread(threadId, options?) — id is the codex internal session_id
(NOT a file path). Persisted at ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl.
Need PLAN mode (read-only thinking before execution)?
└── No SDK primitive. Approximate by spawning the thread with:
sandboxMode: 'read-only' + modelReasoningEffort: 'high'
Then resumeThread() with sandboxMode: 'workspace-write' to execute the plan.
Four rules that get violated most
-
item.updated.item.text is CUMULATIVE, not a delta. Each update carries the entire accumulated body so far. Replace your in-flight buffer; do not append. The same applies to command_execution.aggregated_output and mcp_tool_call.result. This is the #1 cause of streaming-display corruption in our codebase.
-
approvalPolicy MUST be 'never' (and is hardcoded as such in packages/daemon/src/agent/providers/codex.ts). Any other value ('untrusted', 'on-request', 'on-failure') requires the codex binary to read approval responses on stdin — but the SDK closes stdin synchronously after writing the prompt. Other values will hang or auto-fail.
-
Don't try to build a canUseTool equivalent. There is no host-side hook. The interception layer is sandbox + abort, not callbacks. If a feature request asks "show the user the proposed command and let them approve" we can only do it reactively (watch item.started for command_execution, abort the turn, ask the user, start a new turn).
-
Treat the on-disk JSONL as the source of truth, not the live event stream. Codex flushes ~/.codex/sessions/.../rollout-*.jsonl incrementally, and that file is the codex CLI's own log. Our daemon reconciles from it after every turn (see multitable/reconcile-and-jsonl.md) because the live event stream can drop items in edge cases.
Three things the SDK does NOT have (so you can stop looking)
- No
canUseTool. No host-side approval callback. Sandbox + abort is the only gate.
- No
AskUserQuestion, no onElicitation. Not in the type union, not in the event stream, not on any item type. If you need user input mid-turn, abort and start a new turn.
- No
hooks, no PreToolUse/PostToolUse/Notification callbacks. The only observability is the ThreadEvent async generator yielded by runStreamed().
These names belong to the Claude Agent SDK. Don't paste them into Codex code.
Ground-truth files
When in doubt about behavior, these are authoritative (in this order):
node_modules/@openai/codex-sdk/dist/index.d.ts — installed type defs.
node_modules/@openai/codex-sdk/dist/index.js — installed runtime (read this when types are vague, e.g. exact CLI flag emission).
packages/daemon/src/agent/providers/codex.ts — how MultiTable wires the SDK; load-bearing comments at the top of the file.
packages/daemon/src/transcripts/codexParser.ts — JSONL → Message[] parser; the format we trust as "what really happened".
- Codex SDK README on GitHub.
- Codex CLI reference and config reference.
Where this SDK lives in our codebase
packages/daemon/src/
├── agent/
│ ├── manager.ts ← dispatches by provider; for codex calls codexAdapter.runTurn()
│ ├── providers/
│ │ ├── codex.ts ← THE Codex adapter; owns thread cache, event handling, reconcile
│ │ └── types.ts ← AdapterCallbacks contract
│ └── types.ts ← AgentSession (provider: 'claude' | 'codex')
└── transcripts/
└── codexParser.ts ← reads ~/.codex/sessions/.../rollout-*.jsonl into Message[]
The manager itself is a Claude-shaped SDK orchestrator — when adding Codex behavior, the only file that should grow is providers/codex.ts and (for parsing) transcripts/codexParser.ts. The manager exposes a small AdapterCallbacks bag the adapter calls into.