@openai/codex (Rust port, v0.153.2 verified). Distributed as an npm package wrapping a per-platform Rust binary.
Install
baseCLIPostCreate provisioning script.
Auth
Two paths, both measured againstcodex-cli 0.153.2. Codex is not Claude
Code: it has no environment variable for a subscription login, and the one it
has for API keys (CODEX_API_KEY) overrides a stored login.
API key
Codex reaches its provider only through its ownmodel_providers config —
OPENAI_BASE_URL no longer exists in the binary, and the built-in openai
provider refuses to be overridden. Crewship therefore emits a custom provider
per run:
OPENAI_API_KEY in the environment is a dummy; the sidecar replaces it
with the real key from its CredStore. The real key never enters the container.
ChatGPT plan
Runcodex login on your own machine, then store the whole
~/.codex/auth.json as a credential:
refresh_token and id_token become secret parts (the refresh token is
sealed — never revealed, never delivered), account_id, plan, expires_at
and mode identifier parts. The older form — the whole file stored as
--type AI_CLI_TOKEN --provider OPENAI — still works and is delivered the
same way, but it has no owner, no expiry badge and no central refresh; see
Provider logins.
The slot name on assign is the binding’s identity, not something Codex
reads — the login is never exported as a variable.
Or skip the paste: crewship credential login --provider OPENAI prints a
one-time code and a URL, you enter the code in OpenAI’s device page, and the
server finishes the sign-in itself — no codex binary involved — and stores
the resulting auth.json as the same kind of credential, named after the
plan and account. See Sign in with a code.
In the dashboard, Add secret → shape Provider login → brand OpenAI
→ Subscription takes the same file; API key on the same tile stores a
metered key instead. A bare access token is refused at submit time: Codex
insists on id_token, access_token, refresh_token and account_id all
being present, so only the file codex login writes can be delivered.
At every run start Crewship renders the login into the agent’s
$CODEX_HOME/auth.json (/crew/agents/<slug>/.codex/auth.json, mode 0600)
with the real id/access tokens and account id, refresh_token replaced by
rt.crewship-managed-no-refresh, and last_refresh stamped now. Codex only
touches the refresh token when the access token nears expiry, so the file
authenticates normally — and because no container ever holds a real refresh
token, one login can be assigned to any number of agents without the
containers rotating (and invalidating) each other’s tokens. Revoking the
credential removes the file from running containers; unassigning it removes
the file at the next run start.
Subscription traffic goes to chatgpt.com through the sidecar’s CONNECT
tunnel, exactly like Claude Code’s OAuth mode: the sidecar cannot meter it, so
the run is billed flat_rate with CREWSHIP_SUBSCRIPTION_PLAN set from the
token’s chatgpt_plan_type claim (“ChatGPT Plus”). Model policy on the
credential is not enforced in this mode.
Credential-backed OPENROUTER and OPENAI_COMPAT models use Codex’s native
custom model_provider configuration for that invocation. For example,
openrouter/anthropic/claude-sonnet-4 is sent to
/llm/openrouter with anthropic/claude-sonnet-4 as Codex’s provider-local
model ID. The provider must implement the OpenAI Responses API; current
Codex no longer supports wire_api = "chat". Use the OpenCode adapter for a
Chat-Completions-only compatible endpoint.
Models
Codex CLI is a STRICT subset of OpenAI’s general API — only the coding-tuned GPT-5.x/6.x family is accepted:
Codex CLI rejects
o3, o4-mini, gpt-4o etc. — those are general-API only.
Command shape
exec— non-interactive subcommand (NOT--quiet, that flag doesn’t exist)--json— newline-delimited JSON events on stdout--sandbox—read-only(MINIMALprofile) |workspace-write(all other profiles —CODING/FULL) |danger-full-access(never opted into)--separator prevents user message dash-prefixed tokens from being parsed as flags- System prompt is folded into the user message body with
[SYSTEM]/[USER]delimiters because Codexexechas no system-prompt flag
MCP
Codex uses TOML, not JSON. Crewship writes/crew/agents/<slug>/.codex/config.toml (HOME, NOT workdir — Codex requires interactive trust ceremony for project-scoped configs).
Authorization: Bearer ${TOKEN} headers are translated to bearer_token_env_var = "TOKEN". Generic X-API-Key: ${KEY} headers go to env_http_headers = { "X-API-Key" = "KEY" }. Literal headers go to http_headers = { ... }.
Two sidecar-hosted MCP servers are auto-injected into every generated config
(
injectMemoryMCP + injectRoutinesMCP): crewship-memory
(http://127.0.0.1:9119/mcp/memory/<slug>) and crewship-routines
(.../mcp/routines) — both http transport. They give the model native
memory.read/write/search/append_daily and save_routine/list_routines
tools regardless of CLI. A user-declared server of the same name wins (no
overwrite).Memory
Canonical memory file set written to/output/<slug>/AGENTS.md (Codex’s
auto-discovered file). Plus the cross-CLI parity files for an agent
swapping CLI mid-mission. First-turn parity: system prompt is also
prepended to the user message via [SYSTEM]/[USER] delimiters since
Codex doesn’t load AGENTS.md until the second invocation.
Output stream
Per developers.openai.com/codex/cli/reference, Codex emits an envelope-and-item model (NOT the Agents SDK schema):
Item subtypes:
agent_message, reasoning, command_execution (uses aggregated_output + exit_code, NOT output), file_change, mcp_tool_call (uses server/tool/arguments/result), web_search, plan_update, todo_list, collab_tool_call.
Troubleshooting
codex: not found — npm install failed in container. Check journalctl -u prod-server | grep codex.
unknown flag: --quiet — adapter regression; ensure orchestrator pinned to current schema (see cli_adapter_versions_test.go).
MCP server returns 401 — verify bearer_token_env_var references a credential present in the container env. Run crewship agent mcp <slug> --resolved to dump the effective merged MCP config (crew + agent, including each server’s bound credential name) and confirm the expected credential is wired.