OpenAI Codex CLI (headless)
Model: CYBERINECODEXMODEL if set, else Codex's configured default (no -m added). Deliberately unpinned (kd7ax, 2026-09-09): Codex 0.153.4 made GPT-6-Astra the bundled default, and Cyberine keeps...
- Kind: headless coding agent (subprocess)
- Official docs: https://developers.openai.com/codex/cli · /noninteractive · https://github.com/openai/codex
- Cyberine adapter:
CliAgentAdapterviamakeCodexHeadlessAdapter(build workercodex) andmakeCodexAdapter(read-only--json) - Researched/verified: 2026-06-07, Bedrock models + writes-mode gotcha added 2026-07-10 — confidence high.
Updated 2026-07-17 (T623/T625/T631): both factories now forward
Task.imagesas repeated-i <path>args (critical bug fix —task.imageswas documented but never wired); the headless build worker also passes--dangerously-bypass-hook-trust(Codex's stable hooks can otherwise stall headless on a trust-confirmation prompt the approvals-and-sandbox bypass doesn't cover); the read-only variant gained an opt-in--output-schema <path>(structured output). Updated 2026-08-20 (T656, live-verified against the installed codex; corrected 2026-08-21 -- the earlier text said v0.148.0, which was the latest upstream stable at the time, not the installed binary): the headless build worker now also takes--output-schemaviaCYBERINE_CODEX_OUTPUT_SCHEMA; the read-onlymakeCodexAdapteris marked LIBRARY-ONLY (no production call site); unknown-flag rejections from an older codex surface as named flag-compat errors (see integration notes). Updated 2026-08-21 (k2n3k, docs half; re-verified live viacodex exec --help/codex --help, re-verified again against the installed codex-cli 0.149.1):--full-autois REMOVED from both surfaces, not merely deprecated;-a/--ask-for-approvalexists only on top-levelcodex --help(nowon-request|never, the earlieruntrustedvalue is gone) and is absent fromcodex exec --helpentirely, so a headless worker can never reach it — flag table and gotchas below corrected to match. Verified against the installed 0.149.1 binary:--approve-for-me(0.147.0, 2026-08-07, PR #36373) is an ALTERNATIVE approval mode that CONFLICTS with--dangerously-bypass-approvals-and-sandbox(never additive), superseding the archived T572writes-mode deferral; Amazon Bedrock became a built-in provider andcodex exec forkwas added (0.148.0), both confirmed present. Version pinned at codex-cli 0.149.1. Updated 2026-08-26 (k2n3k, evaluation half — closes the task):--approve-for-menow has a written NO-GO verdict plus the exact replacement argv, proven by threecodex execparse probes against the installed 0.149.1 binary that exit on argument parsing alone (no session, no spend) — see "--approve-for-mego/no-go" below.
Cyberine invocation
# build worker (makeCodexHeadlessAdapter)
codex exec --skip-git-repo-check --cd <cwd> -m <model> [-i <img>...] [--output-schema <path>] \
--dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust "<prompt>"
# read-only variant (makeCodexAdapter) — JSONL events; LIBRARY-ONLY, no production call site (T656)
codex exec --json [--output-schema <path>] [-i <img>...] "<prompt>"Model: CYBERINE_CODEX_MODEL if set, else Codex's configured default (no -m added). Deliberately unpinned (kd7ax, 2026-09-09): Codex 0.153.4 made GPT-6-Astra the bundled default, and Cyberine keeps riding Codex's own configured default so the operator's ~/.codex/config.toml stays the single source of truth; the model that actually answered is read from the model: header codex exec prints on stderr (parseCodexEffectiveModel) and recorded as raw.model on the Result, workerRuns[].model in run.json, and model on the provenance candidate. Auth dir allow-listed by the Cyberine sandbox: CODEX_HOME when set, else both ~/.codex (official default) and ~/.config/codex (fixed 2026-06-07 — see gotcha).
Auth
CODEX_API_KEYenv (recommended forexec) orCODEX_ACCESS_TOKEN(ChatGPT/account flows).- Interactive auth stored in
$CODEX_HOME(default~/.codex):auth.json(secret),config.toml(user) or.codex/config.toml(project).CODEX_HOMEoverrides the default.
MCP servers (client config, added kskdf, verified 2026-09-18)
Codex CLI is also an MCP client: ~/.codex/config.toml (or $CODEX_HOME/config.toml; a trusted
project may use .codex/config.toml) takes a [mcp_servers.<name>] TOML table per server, fields
command (required for stdio), args (optional array), env (optional table). Live-verified
against the installed codex-cli 0.154.0: codex mcp --help lists list|get|add|remove|login|logout,
and codex mcp add <name> [--env KEY=VALUE...] -- <command> [args...] writes exactly that table
(--url + --bearer-token-env-var for a streamable-HTTP server instead of stdio). codex mcp add context7 -- npx -y @upstash/context7-mcp is the documented shape. Source:
https://learn.chatgpt.com/docs/extend/mcp?surface=cli (redirected from
https://developers.openai.com/codex/mcp), retrieved 2026-09-18.
cyberine mcp install --target codex writes this table directly (never shells out to codex mcp add, so it works even without codex on PATH) — merges into any existing config.toml by
appending a new table, is idempotent (detects an existing [mcp_servers.cyberine] header and
no-ops), and backs the file up once to config.toml.bak-cyberine before its first write. --print
shows the table without touching the file.
Commands & key flags
| Item | Purpose |
|---|---|
codex exec PROMPT | Non-interactive run; - reads prompt from stdin. Progress → stderr, final answer → stdout. |
codex exec --json PROMPT | JSONL typed events on stdout (thread.started, turn.started, item.completed, turn.completed, turn.failed). |
codex exec resume [ID] | Continue a prior session (--last, --all). |
-m, --model | Override model. |
--sandbox, -s read-only|workspace-write|danger-full-access | Codex's own sandbox (default read-only). |
--ask-for-approval, -a on-request|never | Top-level codex only (interactive TUI) — re-verified absent from codex exec --help entirely on the installed codex-cli 0.149.1 binary; top-level now lists only on-request and never (the earlier untrusted value is gone), no writes value. A headless worker (always codex exec) cannot reach this flag at all — see gotcha below. |
--dangerously-bypass-approvals-and-sandbox (alias --yolo) | Disable BOTH — required headless (stdin closed). |
--dangerously-bypass-hook-trust | Run enabled hooks without requiring persisted hook trust. Required headless once a repo/CODEX_HOME has hooks configured (Codex ships stable hooks per codex features list) — otherwise a hook-trust prompt stalls the same way an unapproved command would. Always passed by makeCodexHeadlessAdapter (T625). |
--skip-git-repo-check | Allow running outside a git repo. |
--cd, -C <path> | Set workspace root before executing (Cyberine pins the tree with this). |
-i, --image <FILE>... | Attach image(s) to the prompt. Cyberine maps each Task.images[].path (resolved against task.cwd) to a repeated -i <path> in both makeCodexAdapter and makeCodexHeadlessAdapter (T623). |
--output-schema <path> | Path to a JSON Schema file describing the final response shape (unlike Claude Code's --json-schema, which takes the schema text inline). Codex injects it as an "## Output schema" instruction section rather than changing the JSONL event shape — the final answer still arrives as a normal agent_message.content string, just schema-constrained JSON text. On the headless build worker (makeCodexHeadlessAdapter) it is opt-in via CYBERINE_CODEX_OUTPUT_SCHEMA / opts.outputSchema (T631, wired T656); on the library-only makeCodexAdapter via opts.outputSchema, combined with that variant's unconditional --json. |
--output-last-message, -o <path>, --ephemeral, -c KEY=VALUE, --oss | Misc. |
Models (Jun 2026)
gpt-5.5 (recommended default), gpt-5.4 (flagship), gpt-5.4-mini (fast/cheap), gpt-5.3-codex-spark (preview, ChatGPT Pro only). gpt-5.2/gpt-5.3-codex deprecated for ChatGPT sign-in (version strings unverified). Underlying API: openai.md.
Bedrock-hosted models (added 0.143.0, verified 2026-07-10 against installed v0.143.0 binary)
gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna — exact ids confirmed via strings on the
installed codex binary at /opt/homebrew/bin/codex (the literal substrings gpt-5.6-sol,
gpt-5.6-terra, gpt-5.6-luna are present). They route through a Codex-native
model_providers.amazon-bedrock provider (endpoint bedrock-mantle.<region>.api.aws, auth via
AWS_BEARER_TOKEN_BEDROCK or a configured bedrock_api_key) — separate from Cyberine's own
BedrockAdapter / loop-bedrock-build path, which talks to AWS Bedrock directly and does not
go through Codex CLI at all.
In scope, documented here as valid CYBERINE_CODEX_MODEL values — makeCodexHeadlessAdapter
already passes CYBERINE_CODEX_MODEL straight through as -m <value> with no allowlist
(src/adapters/CliAgentAdapter.ts), so no code change is required to use these three ids.
Out of scope: Cyberine does not configure Bedrock auth for Codex CLI. The operator's own
~/.codex/config.toml must already set model_providers.amazon-bedrock.aws.region +
AWS_BEARER_TOKEN_BEDROCK (or bedrock_api_key) before CYBERINE_CODEX_MODEL=gpt-5.6-sol (etc.)
will actually reach Bedrock — that setup step is the operator's responsibility, not Cyberine's.
Reasoning-effort enum values confirmed in the binary: minimal, low, medium, high, ultra.
No literal "max" value was found in the binary strings — the "first-class max reasoning
effort" premise could not be corroborated beyond the model ids themselves; not blocking since
reasoning effort is a Codex CLI config knob independent of the model-id passthrough above.
Upstream 0.148.0 (2026-08-18) — Bedrock became a built-in provider, verified on the installed
0.149.1 binary. The release promotes Amazon Bedrock from the operator-configured
model_providers.amazon-bedrock entry above to a Codex-native built-in provider (with
gpt-5.6-sol as its default model) — confirmed since the installed binary is 0.149.1 (>=
0.148.0). Same 0.148.0 release added codex exec fork (fork a saved session
non-interactively) — also now present on the installed binary; no Cyberine touch-point
identified yet.
JSONL event schema (--json)
item.completedwithtype=agent_message→ the answer. Top-levelagent_messageevent uses fieldcontent(string), nottext— Cyberine readsevent['content'].turn.completed→usageobject:input_tokens,cached_input_tokens,output_tokens,reasoning_output_tokens.
Gotchas
- Partial-trust approval mode — was T572's
writes-mode question, now ANSWERED, and the alternative flag is now VERIFIED present. It never shipped as awritesvalue on--ask-for-approval— re-verified live against the installed codex-cli 0.149.1 binary (codex exec --helphas no-aat all; top-levelcodex --helplists onlyon-request|never, the earlieruntrustedvalue is gone), so the original T572 guess would have been wrong to wire. Upstream instead added a standalone--approve-for-meflag in 0.147.0 (2026-08-07, PR #36373; reported semantics:approvals_reviewer=auto_review+approval_policy=on-request+sandbox_mode=workspace-write) — the installed 0.149.1 binary DOES now have this flag (present in both top-levelcodex --helpandcodex exec --help, described as "route approval requests through automatic review using the workspace-write sandbox"). It is an ALTERNATIVE approval mode, never additive: it CONFLICTS with--dangerously-bypass-approvals-and-sandbox, whichmakeCodexHeadlessAdapterpasses unconditionally today — the two cannot both be passed. Evaluated and decided 2026-08-26 (k2n3k): NO-GO, do not wire — the conflict is now proven at clap parse time and the exact replacement argv is recorded, but the headless-stall property that would justify the swap is not establishable without a live session. Full verdict, argv and probe evidence in "--approve-for-mego/no-go" below. --dangerously-bypass-approvals-and-sandboxis required for headless (stdin-closed) — else the first approval stalls indefinitely (GH issues #4565/#9144/#14345; confirmed in Cyberine adapter).--full-autois REMOVED, not merely deprecated — re-verified live: absent from bothcodex exec --helpand top-levelcodex --helpon the installed codex-cli 0.149.1 binary. Use--sandbox workspace-writedirectly. Cyberine's adapters never passed--full-auto(grep-confirmed againstsrc/adapters/CliAgentAdapter.ts), so no code change was needed here.- Config/auth path discrepancy — FIXED (2026-06-07): official
CODEX_HOMEdefaults to~/.codex, butCliAgentAdapter.cliConfigDirs()previously allow-listed only~/.config/codex— so under seatbelt the codex worker could not write to its real config dir. Now honorsCODEX_HOMEwhen set, else allow-lists BOTH~/.codexand~/.config/codex. Verified the generated seatbelt profile grants write to~/.codex. turn.completedusage reports cumulative session totals, not per-call deltas (GH #17539) — diff consecutive events for per-call.cost_usdis read by Cyberine's parse code but is not in the official usage schema — kept as a harmless forward-compat fallback (stays undefined); the parse comment was corrected to stop presenting it as the Codex shape.codex execwithout--json: progress on stderr, answer on stdout — handle the two streams separately.
Credential-refresh progress on the parsed streams (kjcwz, measured 2026-09-04 against 0.153.2)
Codex 0.152.0 added, verbatim: "The terminal UI and codex exec show credential-refresh progress,
including Amazon Bedrock reauthentication" (#41239). New output on a stream Cyberine parses is the
one upstream change class that can corrupt a worker result silently rather than failing loudly, so
the R02 sweep filed it instead of assuming it harmless. Measured verdict: usage is safe on both
factories; the answer TEXT of the headless worker can pick up a stdout progress line, and that is
recorded rather than filtered.
The line texts were read out of the installed binary (strings $(which codex), 0.153.2) rather
than invented: Access token expired or nearly expired, refreshing., refresh token present, attempting refresh, Refreshed access token., Token refresh not possible, re-authorization required., plus the failure strings failed to refresh token while getting account: and OAuth refresh token was rejected:. They sit beside codex's tracing strings in the binary, so stderr is
their likely home — but which stream they land on is exactly what a Cyberine change must not depend
on, so both are exercised.
The two factories have different contracts and the answer differs between them:
makeCodexHeadlessAdapter(the build workercodex) runs plaincodex execand takesstdout.trim()as the answer. Inexecmode codex sends the FINAL MESSAGE to stdout and its reasoning/progress to stderr, so stdout is normally just the answer — which means a progress line that DOES land on stdout is appended into the answer text. Deliberately NOT filtered: matching upstream log wording would rot at the next rewording and could delete legitimate answer text. The smoke pins this as the current contract, so if a future codex starts putting refresh progress on stdout in volume, that check fails loudly first and makes the cost of filtering worth paying.makeCodexAdapter(codex exec --json) decodes events, so the noise is skipped outright: the answer comes from the lastagent_messageand usage fromturn.completed.
Usage is unaffected on both paths, which is the assertion that actually matters — a wrong token
count feeds cost estimation, while extra prose in an answer is visible to a judge. parseCodexHumanUsage
cannot mistake any refresh line for a tokens used summary, and a refresh line landing BETWEEN the
label and its count degrades to "no usage", never to a wrong one. Guard: npm run selftest:codex-usage
section 10 (three sub-sections, offline, stub codex on PATH, no credentials and no network).
--approve-for-me go/no-go (k2n3k, decided 2026-08-26 against codex-cli 0.149.1)
Verdict: NO-GO for now — do not wire. The flag parses cleanly in the exact shape Cyberine would
use, but the single property that matters headless — never stalling on closed stdin — cannot be
established by a parse probe, and the CYBERINE_TASK_TRUSTED=false path it would serve is already
sandboxed twice over.
Exact argv Cyberine would pass (if wired)
--approve-for-me can only REPLACE --dangerously-bypass-approvals-and-sandbox, never join it, so
the candidate CYBERINE_CODEX_APPROVAL=approve-for-me build in makeCodexHeadlessAdapter
(src/adapters/CliAgentAdapter.ts) is exactly today's argv with that one flag swapped:
codex exec --skip-git-repo-check --cd <cwd> [-m <model>] [-i <img>...] [--output-schema <path>] \
--approve-for-me --dangerously-bypass-hook-trust "<prompt>"--dangerously-bypass-hook-trust stays (T625 — it covers a different prompt, and P2 below proves
the two compose). -s/--sandbox is NOT added: --approve-for-me already implies workspace-write,
so passing both parses (P3) but is redundant.
Parse probes (2026-08-26, installed codex-cli 0.149.1 — each exits on argument parsing alone, no model session, no spend)
| Probe | argv | Result |
|---|---|---|
| P1 conflict | codex exec --skip-git-repo-check --approve-for-me --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust "probe" | exit 2, error: the argument '--approve-for-me' cannot be used with '--dangerously-bypass-approvals-and-sandbox'. The conflict is enforced at clap PARSE time — replacement only, provably never additive. |
| P2 replacement | the candidate argv above plus a trailing unknown --cyberine-parse-probe-sentinel | exit 2, error: unexpected argument '--cyberine-parse-probe-sentinel' found, and ONLY that. Every real flag ahead of the sentinel was accepted, so the candidate argv parses clean. |
| P3 sandbox compose | codex exec --approve-for-me -s workspace-write --cyberine-parse-probe-sentinel "probe" | exit 2 on the sentinel only — --approve-for-me does not conflict with -s. |
The sentinel is what makes P2/P3 evidence rather than assumption: clap reports a conflict or a bad value for any flag it rejects, so a run that dies solely on the sentinel proves every preceding flag was accepted.
Why NO-GO despite a clean parse
- The stall question is untestable without spend, and it is the entire reason the bypass flag
exists.
--dangerously-bypass-approvals-and-sandboxis mandatory today precisely because any approval prompt on closed stdin hangs until the timeout.approvals_reviewer=auto_reviewis meant to answer those prompts with a model instead of a human, but whether a reviewer DENIAL falls back to a human prompt cannot be observed by parsing argv — it needs a live session driving a command the reviewer refuses. Guessing wrong yields a silently-killed dispatch, the exact failure class of the 2026-08-26 stuck-detector bug. - It costs model calls the spend cap cannot see. auto_review adjudicates each approval request
with a model call. A Codex worker's usage already bypasses the daily spend cap — the gate is
adapter.info.kind === 'loop'insrc/engine/WorkflowEngine.ts, correct for a subscription Codex and wrong for a metered one (the same reason T682's NO-GO stands). Wiring approve-for-me would add invisible spend on top. - The marginal security gain is near zero on the path it targets.
CYBERINE_TASK_TRUSTED=falsealready REFUSES to spawn a CLI worker without a host sandbox (src/adapters/hostSandbox.ts) and already forcesriskLevel: 'high'with a mandatory L3 adversarial gate (src/engine/WorkflowEngine.ts,src/engine/trustGate.ts). Codex's ownworkspace-writewould be a third layer over a path that cannot run unsandboxed in the first place. workspace-writemay not cover what a build worker actually writes. Beyond--cd <cwd>a worker touches$CODEX_HOME,/tmp, and package/tsx caches. The Cyberine host sandbox allow-lists those explicitly; whether Codex's own workspace-write does is unverified.
What would flip it to GO
One live smoke on a throwaway worktree proving all three: (a) a run completes with
--approve-for-me and stdin closed, (b) a reviewer-denied command surfaces as a normal failure
rather than a hang, (c) the worker's writes to $CODEX_HOME / /tmp / caches are not blocked.
Until then the argv above is recorded so that wiring is mechanical rather than exploratory.
--no-daemon (Codex 0.156.0) — not wired (kdhbf, checked 2026-09-25)
Codex rust-v0.156.0 added a --no-daemon flag. The binary installed on this machine is codex-cli 0.155.1: neither codex --help nor codex exec --help lists the flag, and the only daemon surfaces in --help are codex agents and codex remote-control, which manage the shared app-server daemon for interactive sessions. Passing an unknown flag to codex exec would fail every worker on this install, so the codex worker's argv does not carry it. Revisit on the first upgrade to 0.156+: check whether codex exec attaches to the shared daemon by default there, and wire --no-daemon only if it does (a worker must not share session state with the operator's interactive Codex).
Usage limits and reset time (ktwg5, measured 2026-09-23 against codex-cli 0.155.1)
Reactive: the refusal names its reset. The installed binary's strings carry the two refusal shapes, "You've hit your usage limit. Upgrade to Plus ... or try again at ." and the Pro/credits variant ending "... purchase more credits or try again at .", plus a bare "Try again at ". <when> is %-I:%M %p when the reset falls today ("9:16 AM") and %b %-d<st|nd|rd|th>, %Y %-I:%M %p otherwise ("Sep 24th, 2026 9:16 AM"), both local time. src/core/cliUsageLimit.ts parseUsageLimitResetMs reads either form (and a relative "try again in 2 hours 13 minutes"), and CliAgentAdapter records the cooldown to end exactly then instead of the generic 24-hour cli-unavailable TTL. A clock time already past today rolls to tomorrow; a parse more than 8 days out is discarded as a misread.
Proactive: YES, headroom is readable for free. Every rollout file under $CODEX_HOME/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl carries token_count events with rate_limits: primary (the 5-hour window, window_minutes: 300) and secondary (weekly, window_minutes: 10080), each with used_percent and resets_at in epoch SECONDS, plus a credits block. Measured on this machine: primary 10% used, secondary 63% used. readCodexRateLimits() reads the newest reading without spending a turn; codexExhaustedUntilMs() returns the latest reset of any window at 100%. When a codex refusal carries no parsable time, the adapter falls back to that reading. The reading is only as fresh as the last codex session on the machine, so a quiet machine reports an old number.
Pricing
No per-token Codex CLI price; included in ChatGPT Plus/Pro/Business/Edu/Enterprise. API-key usage billed at standard OpenAI model rates. https://openai.com/pricing
Cyberine integration notes
codexbuild worker gated on install + headless login. Cyberine pins the tree with--cd <cwd>; the host sandbox (seatbelt/bubblewrap) is separate from Codex's own--sandbox.- Plain-text output mode (no
--json) means no usage data for the build worker — Cyberine falls back to stdout/stderr text. - Flag-compat diagnosis (T656): an older installed
codexthat rejects a version-gated flag (clap emitserror: unexpected argument '--flag' found, exit 2) surfaces a named error carrying the flag, the owning task, a known-good version, and the disable knob — registryCLI_FLAG_COMPATinsrc/adapters/CliAgentAdapter.ts, smokenpm run selftest:cli-flag-compat. makeCodexAdapter(the--jsonread-only variant) is LIBRARY-ONLY (T656): no production call site —buildAdapters.tsconstructs the build-fleetcodexworker viamakeCodexHeadlessAdapter.
DeepSeek-backed Codex worker (T682, verified 2026-08-25 against installed codex 0.149.1)
Codex can be pointed at DeepSeek's native Responses API with zero Cyberine code change:
makeCodexHeadlessAdapter passes its model straight through as -m <model>
(src/adapters/CliAgentAdapter.ts:2387, resolved from CYBERINE_CODEX_MODEL at :2375) and has
no model allowlist. It also passes no provider override — there is no -c model_provider=...
in buildArgs — so base_url and wire_api come solely from the Codex config the process reads.
DANGER: do not run DeepSeek's setup script
DeepSeek ships a curl-pipe-to-bash installer that rewrites ~/.codex/config.toml globally. That
file and the codex binary are SHARED with the global nf-codex skill and (on the homelab) with
MONITORING/container-monitor.sh's cron. Running it flips every Codex client — CLI, desktop app,
IDE extension — onto API-key billing and hides all existing ChatGPT-subscription session history,
because Codex partitions history by login method. A suddenly-empty codex exec resume list is that
switch, not data loss. Never run it. Use the scoped recipe below instead.
The scoped recipe — CODEX_HOME fully isolates it
CODEX_HOME relocates the whole Codex config directory, not just a layer. Verified: with
CODEX_HOME set, codex exec reported provider: deepseek / model: deepseek-v4-flash while the
md5 of ~/.codex/config.toml was unchanged across every probe in the session.
export CODEX_HOME="$PWD/.local/tmp/codex-deepseek" # any dir; keep it gitignored, it holds a key
mkdir -p "$CODEX_HOME"
cat > "$CODEX_HOME/config.toml" <<'TOML'
model = "deepseek-v4-flash"
model_provider = "deepseek"
preferred_auth_method = "apikey"
forced_login_method = "api"
model_catalog_json = "<ABSOLUTE PATH>/models.json" # top-level ONLY — see the TOML trap below
[model_providers.deepseek]
name = "DeepSeek"
base_url = "https://api.deepseek.com/"
wire_api = "responses"
experimental_bearer_token = "<DEEPSEEK_API_KEY>"
TOML
chmod 600 "$CODEX_HOME/config.toml"Then dispatch through Cyberine normally — CODEX_HOME reaches the child untouched
(buildScopedChildEnv only filters provider-credential keys, and CODEX_HOME is not one;
src/adapters/cliConfigDirs.ts:59 already honors it):
CODEX_HOME="$CODEX_HOME" CYBERINE_CODEX_MODEL=deepseek-v4-flash CYBERINE_BUILD_FLEET=codex \
npm run wf -- .local/dogfood/t682-codex-deepseek.workflow.tsThe workflow def lives in gitignored .local/dogfood/ (this repo's documented home for defs), so
recreate it if it is not on your machine:
// .local/dogfood/t682-codex-deepseek.workflow.ts
import type { WorkflowDef } from '../../src/engine/workflowDef.js';
const def: WorkflowDef = {
name: 't682-codex-deepseek',
steps: [{ id: 'probe', title: 'DeepSeek-backed Codex probe',
prompt: 'Reply with exactly the single line: T682-WF-OK', agentIds: ['codex'] }],
};
export default def;TOML trap that cost a debugging cycle
model_catalog_json is a top-level key. Appending it after the [model_providers.deepseek]
header makes it model_providers.deepseek.model_catalog_json, which Codex silently ignores — no
error, and the catalog is simply never read. Put every top-level key BEFORE the first table header.
models.json — the required-field schema, discovered by probe
Without a catalog entry Codex warns Model metadata for <id> not found. Defaulting to fallback metadata; this can degrade performance and cause issues. The schema is undocumented; these are the
fields 0.149.1 requires (each one discovered by reading the parser's own missing field error):
{ "models": [ {
"slug": "deepseek-v4-flash",
"display_name": "DeepSeek V4 Flash",
"supported_reasoning_levels": [],
"shell_type": "default",
"visibility": "list",
"supported_in_api": true,
"priority": 1,
"support_verbosity": false,
"experimental_supported_tools": [],
"truncation_policy": { "mode": "tokens", "limit": 100000 },
"base_instructions": "You are a coding agent running in the Codex CLI.",
"context_window": 1000000,
"tool_mode": "direct",
"multi_agent_version": "v1"
} ] }With that file the metadata warning disappears and the model runs. Notes:
base_instructions(ormodel_messages.instructions_template) is REQUIRED — a catalog entry must carry the model's whole system prompt. That is why DeepSeek ships a generator script rather than a snippet, and it is the main cost of hand-maintaining a catalog.truncation_policytakesmode, nottype.supported_reasoning_levelsis an array ofReasoningEffortPresetstructs, not strings;[]is accepted and is the simplest correct value.shell_typevariants:default | local | unified_exec | disabled | shell_command.tool_modevariants:direct | code_mode | code_mode_only.multi_agent_version(v1/v2) is a PER-MODEL field inmodels.json, not a top-levelconfig.tomlkey. Third-party reports namemulti_agent_version = "v1"as the workaround for Codex Multi-Agent V2 silently discarding subagent task payloads on DeepSeek, without saying where it goes; this is where. The underlying bug was NOT re-verified on 0.149.1 — the reports are against 0.148.0 and remain third-party.wire_api = "chat"is rejected outright by 0.149.1 ("wire_api = \"chat\" is no longer supported."), soresponsesis not a preference here, it is the only option.
Operator gotcha: codex exec blocks on stdin
Run by hand, codex exec prints Reading additional input from stdin... and waits — a 5-minute
hang with no output that looks like a network stall. Append </dev/null. Cyberine is immune:
CliAgentAdapter spawns with stdio: ['ignore', 'pipe', 'pipe'].
Known limitations and deferred work
-
No usage is recorded for a Codex worker.FIXED 2026-08-25 (T694). The adapter now parses the token summary, so acodexdispatch recordsusage.outputTokensinstead ofusage: null. Corrections to the original report, from readingcodex-rs/exec/src/event_processor_with_human_output.rsat tagrust-v0.149.1(print_final_output), which is the code that emits it:eprintln!("{}\n{}", "tokens used".style(self.dimmed), format_with_separators(blended_total(usage)))- It is stderr, not stdout (
eprintln!). The earlier note said stdout. - The label and the count are on separate lines, and the label is bare — there is no
tokens used: Nform to match. - The label may be ANSI-dim wrapped (
ESC[2m…ESC[0m) even when stderr is a pipe:--color autodefers tosupports_coloron the stderr stream, which honours env such asFORCE_COLOR. Observed on a real 0.149.1 run on the homelab box: its stderr came back carryingESC[2m…ESC[0maround every dim-styled line even though stderr was a pipe, so "piped, therefore no ANSI" is not a safe assumption. - The count is locale-grouped:
format_with_separatorsisicu_decimaloversys_locale::get_locale()(en-US only as a fallback), so the separator is,on en-US,.on de-DE/vi-VN, U+00A0/U+202F on fr-FR,'on de-CH.parseCodexHumanUsageaccepts all of them and requires the last group to be exactly 3 digits, which is what keeps18.12from being read as a count. - It is one blended number:
blended_total()is(input_tokens - cached_input_tokens) + output_tokens. No input/output split exists in the human output, so the total is reported asoutputTokens(conservative —estimateCostUsdprices output above input) andinputTokensis left absent rather than invented. - It is the cumulative session total, not a per-turn delta — harmless because the adapter
cold-spawns a fresh
codex execper dispatch, but a futureresume-based warm session would have to diff it.
Pinned by
npm run selftest:codex-usage(captured stderr fixtures + an end-to-end dispatch against a stubcodexon PATH, no credentials needed). - It is stderr, not stdout (
-
cyberine run(the single-step path) prints arunIdandstate: donebut writes norun.json—orchestrate()does not persist. Only the workflow engine (npm run wf) does. Use a workflow def when you need a recorded artifact. -
A step's
agentIds: ['codex']did not restrict the fleet in the T682 probe — six workers ran. Observed, not diagnosed.
go/no-go as a standing build-fleet worker
NO-GO for now, on cost-shape grounds rather than capability. The path works end-to-end (recorded
run 44320a3b, status=success), but a DeepSeek-backed Codex worker is token-metered against
DEEPSEEK_API_KEY, whereas the default codex worker is subscription-billed against the
user's ChatGPT plan. Registering it as a standing worker would silently move a lane from flat-rate
to metered, and the spend it generates would not reach the daily cap.
Still NO-GO after T694, for the second half of that reason only. Usage parsing landed
(usage.outputTokens is now recorded), but recorded usage is not the same as capped spend:
WorkflowEngine's recordRealizedSpend() call is gated on adapter.info.kind === 'loop', so NO
CLI worker's cost enters CYBERINE_SPEND_CAP_USD's cumulative tally. That gate is right for the
default codex worker (subscription-billed, no metered cost to count) and wrong for a
DeepSeek-backed one — and the adapter cannot currently tell the two apart, since the provider comes
from whatever CODEX_HOME config the subprocess reads, not from anything Cyberine passes. Teaching
the cap to distinguish a metered CLI worker from a subscription-funded one is the remaining
prerequisite; the invisible-usage half is done.
Sources
- https://developers.openai.com/codex/cli/reference
- https://raw.githubusercontent.com/openai/codex/rust-v0.149.1/codex-rs/exec/src/event_processor_with_human_output.rs
(
print_final_output— thetokens usedemit site, read for T694) - https://raw.githubusercontent.com/openai/codex/rust-v0.149.1/codex-rs/protocol/src/num_format.rs
(
format_with_separators— the locale-aware digit grouping) - https://developers.openai.com/codex/noninteractive
- https://developers.openai.com/codex/agent-approvals-security
- https://developers.openai.com/codex/models
- https://learn.chatgpt.com/docs/extend/mcp?surface=cli (MCP client config, retrieved 2026-09-18)
- https://github.com/openai/codex
Claude Code CLI (headless)
Default model: claude-sonnet-5-5 (CYBERINECLAUDEMODEL; was claude-sonnet-5 until 2026-10-01, kvhxs). Auth dir allow-listed by the sandbox: ~/.claude.
Google Gemini CLI (headless)
Default model: gemini-2.5-flash (CYBERINEGEMINIMODEL / GEMINIMODEL). Auth dir allow-listed by the sandbox: ~/.gemini.