Summary
Codex CLI 0.156.0 introduced a "workspace routing discovery" step. It treats tokens.account_id in ~/.codex/auth.json as the selected workspace and verifies it against the workspaces that discovery returns for the ChatGPT account derived from the token claims. codex-multi-auth writes the org id into that field for accounts whose accountIdSource is org, so discovery cannot find it and every request fails — regardless of which model is requested. Codex 0.154.0 is unaffected, because it did not perform this lookup.
This is not a model-specific or account-specific problem: on my machine 7 of 8 managed accounts are affected, and the only unaffected one is the single account whose stored accountId happens to equal its token's chatgpt_account_id claim.
I want to flag up front that writing the org id looks deliberate in the current design — resolveRequestAccountId is documented as "Preserves explicit org/manual selections; only token/id_token sources auto-follow token changes." So this is a semantics change on the Codex CLI side that the current design predates, not a simple coding slip. That is also why I am not sending a patch blindly: the right fix depends on how you want workspace selection expressed under the new contract (see "Possible direction" below).
Reproduction
- Add a ChatGPT account and select an org/team workspace, so the stored account has
accountIdSource: "org" and accountId: "org-…".
- Let
codex-multi-auth sync the selection into ~/.codex/auth.json (the written file contains codexMultiAuthSyncVersion and tokens.account_id = "org-…").
- Install Codex CLI
0.156.0 or 0.157.0-alpha.10.
- Run any request against the official binary, e.g.
codex exec --skip-git-repo-check "reply with exactly: OK".
Expected Behavior
The request succeeds, as it does on Codex CLI 0.154.0 with the exact same auth.json.
Actual Behavior
warning: Falling back from WebSockets to HTTPS transport. selected workspace missing from routing discovery
ERROR: Reconnecting... 1/5
...
ERROR: selected workspace missing from routing discovery
Reproduced with the default model and with explicitly selected models; the model is irrelevant. Running the older 0.154.0 binary directly against the same auth.json succeeds immediately.
Evidence
Comparing the codex-multi-auth-written auth.json with one written by a native codex login (values redacted, shapes preserved):
|
tokens.account_id |
chatgpt_account_id claim in tokens.id_token |
result |
codex-multi-auth synced |
org-XXXXXXXX… (org id) |
<uuid> |
fails |
native codex login |
<uuid> |
<uuid> (same value) |
works |
Decoding each stored account's accessToken claims and comparing them with the stored accountId:
- 7 of 8 accounts: stored
accountId is org-…, token claim chatgpt_account_id is a UUID → mismatch
- 1 of 8 accounts:
accountIdSource: "token", both values identical → match, and this is the only account that works
Relevant code on main (2.16.0):
lib/codex-cli/writer.ts:454 — nextTokens.account_id = selection.accountId.trim(); writes the stored id verbatim, org ids included.
lib/auth/token-utils.ts:361-384 — shouldUpdateAccountIdFromToken / resolveRequestAccountId intentionally keep org-sourced ids instead of following the token claim.
What I could not verify: I wanted to run the decisive single-variable A/B (same credentials, only tokens.account_id swapped from the org id to the token's chatgpt_account_id). The native codex login I ran to recover my machine invalidated the previously active account's refresh token (token_revoked / refresh_token_invalidated), so the backed-up credentials were dead before I could run it, and the stored accounts carry no id_token to rebuild a valid auth.json from. So the causal link is inferred from the structural correlation plus the error semantics, not from a controlled experiment. If you can run that swap on a live org-selected account, it should settle it in one step.
Possible direction (for discussion, not a proposal I have validated)
Codex 0.157.0-alpha.10 ships a forced_chatgpt_workspace_id config.toml key, and its own validation message shows UUID-shaped workspace ids:
forced_chatgpt_workspace_id must be a single workspace ID string or a TOML list of strings; comma-separated strings are not supported. Use `forced_chatgpt_workspace_id = ["123e4567-e89b-42d3-a456-426614174000", …]` instead.
That suggests the new contract splits the two concerns that tokens.account_id used to carry:
tokens.account_id → the ChatGPT account id (must match the token claim)
- workspace/org pinning →
forced_chatgpt_workspace_id
If that reading is right, preserving today's behavior under new Codex versions would mean writing the claim-derived account id into auth.json and expressing the org selection through the workspace key instead. Two things I do not know and did not want to guess at:
- whether
forced_chatgpt_workspace_id accepts org-… ids or only the UUID workspace ids, and
- whether the change needs to be conditional on the detected Codex CLI version, since
0.154.0 and earlier appear to rely on the old meaning.
Happy to prepare a PR once you indicate the direction you prefer.
Environment
codex --version: codex-cli 0.157.0-alpha.10 (also reproduced on 0.156.0; 0.154.0 unaffected)
codex-multi-auth: failure observed on 2.15.0; code references above are from main / 2.16.0
codex-multi-auth status / check / report --json / doctor --json: intentionally omitted — my machine is currently on a native codex login that works, and I did not want to run commands that may re-sync auth.json and break it again. I can collect them on request in a throwaway CODEX_HOME.
- OS: macOS 26.5.1 (arm64)
- Node.js: v22.22.3
Compliance Confirmation
Summary
Codex CLI
0.156.0introduced a "workspace routing discovery" step. It treatstokens.account_idin~/.codex/auth.jsonas the selected workspace and verifies it against the workspaces that discovery returns for the ChatGPT account derived from the token claims.codex-multi-authwrites the org id into that field for accounts whoseaccountIdSourceisorg, so discovery cannot find it and every request fails — regardless of which model is requested. Codex0.154.0is unaffected, because it did not perform this lookup.This is not a model-specific or account-specific problem: on my machine 7 of 8 managed accounts are affected, and the only unaffected one is the single account whose stored
accountIdhappens to equal its token'schatgpt_account_idclaim.I want to flag up front that writing the org id looks deliberate in the current design —
resolveRequestAccountIdis documented as "Preserves explicit org/manual selections; only token/id_token sources auto-follow token changes." So this is a semantics change on the Codex CLI side that the current design predates, not a simple coding slip. That is also why I am not sending a patch blindly: the right fix depends on how you want workspace selection expressed under the new contract (see "Possible direction" below).Reproduction
accountIdSource: "org"andaccountId: "org-…".codex-multi-authsync the selection into~/.codex/auth.json(the written file containscodexMultiAuthSyncVersionandtokens.account_id = "org-…").0.156.0or0.157.0-alpha.10.codex exec --skip-git-repo-check "reply with exactly: OK".Expected Behavior
The request succeeds, as it does on Codex CLI
0.154.0with the exact sameauth.json.Actual Behavior
Reproduced with the default model and with explicitly selected models; the model is irrelevant. Running the older
0.154.0binary directly against the sameauth.jsonsucceeds immediately.Evidence
Comparing the
codex-multi-auth-writtenauth.jsonwith one written by a nativecodex login(values redacted, shapes preserved):tokens.account_idchatgpt_account_idclaim intokens.id_tokencodex-multi-authsyncedorg-XXXXXXXX…(org id)<uuid>codex login<uuid><uuid>(same value)Decoding each stored account's
accessTokenclaims and comparing them with the storedaccountId:accountIdisorg-…, token claimchatgpt_account_idis a UUID → mismatchaccountIdSource: "token", both values identical → match, and this is the only account that worksRelevant code on
main(2.16.0):lib/codex-cli/writer.ts:454—nextTokens.account_id = selection.accountId.trim();writes the stored id verbatim, org ids included.lib/auth/token-utils.ts:361-384—shouldUpdateAccountIdFromToken/resolveRequestAccountIdintentionally keeporg-sourced ids instead of following the token claim.What I could not verify: I wanted to run the decisive single-variable A/B (same credentials, only
tokens.account_idswapped from the org id to the token'schatgpt_account_id). The nativecodex loginI ran to recover my machine invalidated the previously active account's refresh token (token_revoked/refresh_token_invalidated), so the backed-up credentials were dead before I could run it, and the stored accounts carry noid_tokento rebuild a validauth.jsonfrom. So the causal link is inferred from the structural correlation plus the error semantics, not from a controlled experiment. If you can run that swap on a live org-selected account, it should settle it in one step.Possible direction (for discussion, not a proposal I have validated)
Codex
0.157.0-alpha.10ships aforced_chatgpt_workspace_idconfig.toml key, and its own validation message shows UUID-shaped workspace ids:That suggests the new contract splits the two concerns that
tokens.account_idused to carry:tokens.account_id→ the ChatGPT account id (must match the token claim)forced_chatgpt_workspace_idIf that reading is right, preserving today's behavior under new Codex versions would mean writing the claim-derived account id into
auth.jsonand expressing the org selection through the workspace key instead. Two things I do not know and did not want to guess at:forced_chatgpt_workspace_idacceptsorg-…ids or only the UUID workspace ids, and0.154.0and earlier appear to rely on the old meaning.Happy to prepare a PR once you indicate the direction you prefer.
Environment
codex --version:codex-cli 0.157.0-alpha.10(also reproduced on0.156.0;0.154.0unaffected)codex-multi-auth: failure observed on2.15.0; code references above are frommain/2.16.0codex-multi-auth status/check/report --json/doctor --json: intentionally omitted — my machine is currently on a nativecodex loginthat works, and I did not want to run commands that may re-syncauth.jsonand break it again. I can collect them on request in a throwawayCODEX_HOME.Compliance Confirmation