# Codex support (OpenAI Codex CLI)

Codex uses the same machinery as the [Claude pool](../README.md), with these translations:

| Claude side | Codex side |
| --- | --- |
| `bin/claude` shim, `claude-accounts` CLI | `bin/codex` shim, `codex-accounts` CLI |
| pool `~/.claude-accounts` | pool `~/.codex-accounts` |
| `CLAUDE_CONFIG_DIR` per-account dirs | `CODEX_HOME` per-account dirs |
| `.credentials.json` / macOS Keychain item (OAuth, machine-local) | `auth.json` (ChatGPT OAuth, machine-local) |
| `claude -p` auto-retry | `codex exec` auto-retry |
| interactive `claude` auto-resume in tmux | interactive `codex` auto-resume in tmux |
| Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets |
| `CLAUDE_*` pool controls | equivalent `CODEX_*` controls |

Same selection rule (session gate first — `CODEX_MULTIACC_SESSION_GATE`, default 50 —
then the 30-point weekly-headroom band with random spread, ≥90% any-bucket
exclusion, peers rotate), same marker semantics (`.limited` cooldowns, `.expired` parks with
credential/policy scoping and soft expiry), same fail-open guarantees, same sync
safety guards. The `codex` shim engages the buffered auto-retry only for
`codex exec` runs with finite stdin, exactly like `-p` on the claude side. Interactive
`codex` in tmux gets [auto-resume](AUTORESUME.md) instead, like `claude`.

Codex-specific notes:

- **Auth is the Codex device-code sign-in by default**: `codex-accounts add` prints
  a URL + one-time code you can open in ANY browser (this machine, your laptop, a
  phone), so it works identically on a local Mac, over SSH, and on servers — there
  is no portable setup-token equivalent for Codex, so a server account is signed in
  ON the server the same way. `--browser` opts into the localhost browser-callback
  flow instead (only works when the browser runs on the same machine — the callback
  goes to `localhost:1455`). Either way the login runs with `CODEX_HOME` pointed at
  the account dir, and identity is verified offline from the id-token before
  anything is registered.
- **`auth.json` is never synced** in either direction, for the same reason
  `.credentials.json` never is: the refresh token rotates, and two machines
  refreshing one grant strand each other. Sync pushes manifest + `config.toml`
  seeds + advisory limit state only.
- **Windows are classified by length**, not by name: any window ≤6h counts as the
  self-healing session signal, anything longer as durable/weekly. A hard
  `limit_reached`/`allowed:false` verdict marks the account even if no window shows
  ≥90%, and a reshaped payload is still scanned recursively for window-shaped
  objects (fail open if nothing parses).
- **Idle-account telemetry** stays fresh the same way: a long-expired access token is
  renewed via the OAuth refresh-token grant (`auth.openai.com/oauth/token`, the CLI's
  own public client id) and the rotated credential is persisted 0600. Overrides for
  tests: `CODEX_MULTIACC_TOKEN_URL`, `CODEX_MULTIACC_CLIENT_ID`,
  `CODEX_MULTIACC_USAGE_URL`.
- **Earned usage resets are redeemed automatically for a parked account:** a bucket
  at/over the pool's exclusion threshold (90%), a limit the backend reports finished, or
  an active usage park (a client-reported 429) that lifts more than an hour from now. The limits refresher checks the reset
  credits belonging to that same account, uses the soonest-expiring available credit,
  and writes a per-account/window idempotency key before redemption so a lost response
  or simultaneous fleet poll cannot spend a second reset. Set
  `CODEX_MULTIACC_AUTO_RESET=0` only for emergency rollback/testing.
  A finished-limit verdict takes precedence over a conflicting percentage. The
  redemption key excludes unused model windows whose reset clocks move between reads.
  Eligible accounts, including spent ones, check the credit endpoint every five
  minutes even if the usage summary still says zero credits. A newly granted reset
  is redeemed during that pass and clears the old limit; no-credit results are
  checked again next pass, without waiting for the weekly reset date.
  All authenticated accounts also report `usage.reset_credits_available` and
  `usage.reset_credits_fetched_at` (ISO 8601) in `list/status/limits --json`.
  The count comes from the credit endpoint, including below 95% used and with
  automatic redemption disabled. The scheduled usage cadence bounds these reads.
  Redemption shares that read, then reads back the remaining count after a POST;
  no subtraction or embedded usage summary substitutes for provider evidence.
  Zero means no available resets; missing fields mean the read was unavailable.
  Credit identifiers remain local and selection does not use these fields.
- **Auto-resume never parks a codex account for the week.** A codex usage-limit error
  in an interactive session writes only the 10-minute `error-cooldown`, the same as
  `codex exec` auto-retry. It never writes a sticky `client:7d` marker. Codex
  `limits.json` is not shared between machines, so a peer's reset redemption can land in
  the gap, and `usage_limit_exceeded` also comes from model-scoped limits. The
  five-minute limits pass marks the account from real telemetry.
  - Turn auto-resume off with `touch ~/.codex-accounts/autoresume.off` (running sessions
    too) or `CODEX_MULTIACC_AUTORESUME=0`.
  - Codex `/goal` state is per account and does not follow a rotation yet.
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
  addon's no-API-keys rule.

```bash
codex-accounts add                    # sign in a new ChatGPT account (device code —
                                      # open the URL in any browser, enter the code)
codex-accounts add --browser          # localhost browser-callback flow instead
codex-accounts import you@example.com --id acct-01
codex-accounts adopt acct-01
                                      # adopt this machine's existing ~/.codex login
codex-accounts list
codex-accounts status
codex-accounts verify --quick         # check local auth without inference
codex-accounts verify                 # real codex exec call per account
codex-accounts --help                 # all account commands and options
claude-multiacc codex status          # same status command via the npm wrapper
```

## MCP servers for every account

`codex-accounts mcp add|add-json|remove|list|apply|import-local` manages the pool's MCP
registry (`~/.codex-accounts/mcp-servers.json`); `add`/`remove` default to `--provider both`,
so the claude pool beside it gets the same change, and `mcp remove` retires the server in
every account (`--user-only` keeps a claude project-local copy). A stock `codex mcp add|remove`
run under the shim — or from inside a pooled codex session — is mirrored into the registry and
every account; on a replica pool the mirror lands in the machine-local overlay instead, so
make registry changes on the source. The registry holds Claude-style blocks; for codex each
becomes a `[mcp_servers.<name>]` table in every account's `config.toml`, and unrelated
tables, their comments and blank lines are left alone: `command`/`args` as is, literal `env`
values as an inline `env` table, a `KEY=${KEY}` reference as `env_vars = ["KEY"]` (codex
forwards a variable under its own name only and expands nothing, so `KEY=${OTHER}` and
`${VAR:-default}` cannot be expressed and are left out with a warning), `headers` on an HTTP
server as `http_headers` (literal) / `env_http_headers` (references), `Authorization: Bearer
${VAR}` as `bearer_token_env_var`, and, for a stdio server, `startup_timeout_sec = 60` unless
the block sets one (a cold `npx` download must not trip codex's 10 s default). Codex-only keys
codex itself wrote (`oauth`, `enabled_tools`, …) ride along in the registry block and never
reach `.claude.json`. SSE transports cannot be expressed and are skipped with a warning;
project scope is claude-only because Codex 0.156 reads no per-project MCP config. Python
3.11+ is found the same way as for `configure`. `CODEX_MULTIACC_MCP=0` stops the reconcile at
seed and launch and the mirror; the CLI keeps working. The codex registry syncs only to codex
sync targets — a runner Mac's local-only codex pool gets its servers from app-robot's runner
overlay (`mcp import-local --owner app-robot`) or a direct `codex-accounts mcp add
--provider codex …` there.

## Opt-in subagent concurrency policy

The addon does not choose delegation policy by default. To manage the concurrent
subagent ceiling across one Codex pool, explicitly configure it (Python 3.11+):

```bash
codex-accounts configure --max-subagents 7 --include-global --json
codex-accounts configure --max-subagents 7 --include-global --check --json
```

An explicitly selected modern Python is preserved. If the shell resolves an older
system Python, this command tries already installed Python 3.11+ executables on
PATH and in standard Homebrew locations, with bounded probes and no downloads.

The first command sets `features.multi_agent_v2.max_concurrent_threads_per_session`
and the compatible `agents.max_concurrent_threads_per_session` in every manifest
account's `config.toml`, migrating its legacy `agents.max_threads` key when present.
An existing boolean `multi_agent_v2` becomes a table with the same `enabled` value;
an existing feature cap is updated along with the agents cap.
`--include-global` also updates the machine's `~/.codex/config.toml`; without it,
the global file is untouched. Unrelated models, instructions, MCP servers, project
trust and custom agents remain intact. Retired directories outside the manifest
are never changed. This is a concurrency ceiling per parent session, not an
instruction to spawn that many agents or a pool-wide account limit.

An opted-in pool persists `codex-settings-policy.json`. Future account seeding and
local sync apply that policy to existing and new accounts. Normal remote sync
explicitly configures and verifies the same pool policy on each target using its
installed addon; update the target addon before syncing. A failed policy readback
fails sync instead of reporting success. Remote sync does not change the target's
global config. Without the policy file, existing settings keep their usual
copy-only-if-missing behavior and no new remote command runs.

`--check` never writes; it checks both caps against the persisted policy
and requested value in selected configs, returning exit 1 on drift. Without
`--max-subagents`, the command only reads the current pool policy and configuration. JSON reports contain
the installed `plugin_version`, `max_subagents`, `policy_max_subagents`,
`accounts_total`, `accounts_configured`, `global_configured`, `mismatch_count` and
`verified`, without account identities, paths, credentials or arbitrary config.

Conflicting profile overrides are rejected.
Unusual inline-table layouts are also rejected before configuration changes;
use ordinary `[agents]` and `[features.multi_agent_v2]` tables for the managed settings.
All target configs are validated before writes, each replacement is atomic, and repeating the command
repairs an interrupted write. Existing running sessions retain the settings they
loaded; the policy applies to subsequent sessions.
