Codex 0.129.0+ rejects fresh ChatGPT token for Models and codex_apps; 0.128.0 works
What version of Codex CLI is running?
The installed version is 0.147.0.
I isolated the regression boundary:
0.128.0: the Models request is authenticated,codex_appsstarts without an authentication error, and a core request returnsOK.0.129.0: Models andcodex_appsreturn HTTP 401token_expired, while the core Responses request still returnsOK.- Sampled releases from
0.130.0through0.147.0show the same 401 behavior.
What subscription do you have?
ChatGPT authentication. The plan was not verified during this test.
What platform is your computer?
Headless ARM64 Linux devbox:
Linux 6.1.155 aarch64
What issue are you seeing?
A fresh ChatGPT login is accepted by the core Responses service, but it is rejected by Models discovery and the built-in codex_apps MCP service.
codex login status reports:
Logged in using ChatGPT
On 0.129.0+, Models discovery returns:
HTTP 401 Unauthorized: Provided authentication token is expired.
url: https://chatgpt.com/backend-api/codex/models?client_version=...
auth error code: token_expired
The codex_apps initialize request returns the same token_expired response.
The token is fresh. Its JWT issue and expiry times are valid, the system clock is correct, CODEX_ACCESS_TOKEN is unset, and no proxy variables are configured. The OpenAI issuer /userinfo endpoint accepts the token. The ChatGPT Models and Apps services reject it. Core Codex requests still work with the same login.
After repeated 401 handling, 0.147.0 can also report:
refresh_token_reused
Your refresh token has already been used to generate a new access token.
Disabling Apps is a temporary workaround:
codex --disable apps
This lets 0.147.0 complete core requests, but Models discovery still returns 401.
What steps can reproduce the bug?
After a fresh ChatGPT login, run these from the same directory and account:
npx --yes @openai/codex@0.128.0 exec \
--ignore-user-config \
--ephemeral \
--skip-git-repo-check \
--sandbox read-only \
"Reply with only OK."
npx --yes @openai/codex@0.129.0 exec \
--ignore-user-config \
--ephemeral \
--skip-git-repo-check \
--sandbox read-only \
"Reply with only OK."
Observed result:
0.128.0receives the model list. It cannot parse the newermaxreasoning value, then falls back and returnsOK. No Apps authentication error appears.0.129.0receives401 token_expiredfrom Models andcodex_apps, then the core request returnsOK.
The failure reproduces with --ignore-user-config, so it does not depend on config.toml.
What is the expected behavior?
A fresh ChatGPT token that works with the core Responses service should also work with Models discovery and codex_apps. Codex 0.129.0+ should behave like 0.128.0 for the same account, machine, network, and token.
Additional information
Diagnostic logs and the doctor report were uploaded through /feedback.
Uploaded thread: 01a002de-bd2e-7041-b2ff-01fb19666c78
The 0.129.0 release includes changes related to Codex Apps authentication and plugin authentication refresh. A different report, #24511, also identifies 0.128.0 as working and 0.129.0+ as failing, though that report has a different network-level symptom.
2 Comments
The refresh_token_reused bit is the interesting one, it usually means the client rotated the refresh token twice, which happens when two processes (app-server + CLI) share the same auth state and both try to refresh. Check for a stale codex or app-server process still holding the old token, kill it, log in once, and it should stop fighting itself. The 0.129.0 boundary narrowing is solid work either way, hopefully the auth-refresh change gets walked back.
The
refresh_token_reusedtail of this is diagnosable from the auth code: refresh serialization only exists in-process.AuthManagerguards refreshes withrefresh_lock: Semaphore::new(1)(https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/login/src/auth/manager.rs#L2001 /#L2137), butauth.jsonis shared by every codex process — CLI, the persistent app-server daemon, MCP subprocesses, and your side-by-sidenpx 0.128.0/0.147.0runs. When one process refreshes, the refresh token rotates on disk; any process still holding the pre-rotation snapshot first presents a superseded access token (the backend reports it astoken_expiredeven though your current JWT is fresh) and then attempts refresh with the already-consumed refresh token →refresh_token_reused, which maps toRefreshTokenFailedReason::Exhausted(#L1604) and forces the failure you see.The Models-discovery/
codex_appssplit fits the same shape: those services hold their own auth snapshot from startup rather than re-reading before each request, so after core rotates tokens they keep sending stale bytes — which is why core requests succeed while Models/Apps 401, and why--disable appsreduces the churn.Fix outline: (1) cross-process refresh serialization (file lock around
auth.jsonrotation) plus reread-on-401-before-refresh; (2) have Models/Apps pull tokens from the liveAuthManagerper request instead of startup snapshots.