fix(auth): codex exec aborts on transient Responses WebSocket 401 with valid ChatGPT credentials
Summary
codex exec intermittently failed during the Responses WebSocket handshake with repeated 401 Unauthorized responses even though ChatGPT authentication was configured and the cached access token was unexpired, had the correct API audience, and was accepted unchanged by later isolated requests.
The failure has not reproduced again, so the exact server-side rejection reason remains unknown. The useful regression boundary is narrower: if a Responses WebSocket handshake temporarily returns 401 for an otherwise usable ChatGPT credential, the noninteractive run should recover without requiring the user to replace that credential.
Steps to reproduce
The failure is intermittent. I have one captured occurrence but cannot force it on demand.
- Sign in with ChatGPT and confirm the CLI sees the cached login:
``sh``
codex login status
- Start a noninteractive run from an isolated
CODEX_HOMEcontaining the same file-backed ChatGPT credentials. The failing run used the equivalent of:
``sh``
codex \
--ask-for-approval never \
--search \
--model gpt-5.6-luna \
-c 'model_reasoning_effort="max"' \
-c 'cli_auth_credentials_store="file"' \
exec \
--json \
--ignore-user-config \
--ignore-rules \
--skip-git-repo-check \
--ephemeral \
-C "$TMPDIR/codex-auth-probe" \
-
- Send a small prompt on stdin.
- In the captured occurrence, the WebSocket upgrade returned 401 seven times and the command exited 1.
The smallest missing reproduction fact is what made the OpenAI Responses WebSocket endpoint reject that specific valid ChatGPT bearer token for roughly ten seconds. The response body, auth-error headers, request ID, and cf-ray were not present in the saved CLI output.
A deterministic regression test could model the observed boundary by returning 401 for the first authenticated WebSocket upgrade and accepting the same credential on a later bounded attempt or fallback transport.
Expected behavior
A temporary 401 during the Responses WebSocket handshake should not permanently fail codex exec when the same unchanged ChatGPT credential remains usable. Codex should recover through its bounded authentication or transport recovery path and complete the request.
The red/green closure signal is an integration test where the first authenticated WebSocket upgrade returns 401, a subsequent bounded attempt accepts the same credential, and the turn completes without changing login state.
Actual behavior
The captured run logged seven handshake failures and exited 1:
codex thread: [REDACTED]
codex turn started
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized, url: wss://api.openai.com/v1/responses
codex engine failed (1)
The failures ran from 2026-08-20T03:57:21Z through 03:57:30Z. No actionable auth message, response body, request ID, or transport fallback appeared in the captured output.
Affected area
- Codex CLI noninteractive
exec - ChatGPT file-backed authentication
- Responses API WebSocket handshake and retry handling
- Likely ownership paths:
codex-rs/codex-api/src/endpoint/responses_websocket.rscodex-rs/core/src/client.rscodex-rs/core/src/responses_retry.rscodex-rs/login/src/auth/manager.rs
Runtime or environment
- Codex CLI:
0.148.0, the latest release when tested - Subscription: ChatGPT Pro
- Authentication: ChatGPT, file-backed
auth.json - Stored API key: false
- Model provider: OpenAI
- Model:
gpt-5.6-luna - Platform:
Darwin 25.5.0 arm64 - Terminal: Ghostty
1.3.1 - Install method: npm
- Proxy environment variables: none
Sanitized codex doctor --json results after the incident:
{
"auth.credentials": {
"status": "ok",
"summary": "auth is configured",
"details": {
"auth storage mode": "File",
"stored API key": "false",
"stored ChatGPT tokens": "true",
"stored auth mode": "chatgpt"
}
},
"network.env": {
"status": "ok",
"details": {
"proxy env vars": "none"
}
},
"network.websocket_reachability": {
"status": "ok",
"summary": "Responses WebSocket handshake succeeded",
"details": {
"auth mode": "chatgpt",
"handshake result": "HTTP 101 Switching Protocols",
"model provider": "openai",
"supports websockets": "true",
"wire API": "responses"
}
},
"runtime.provenance": {
"status": "ok",
"details": {
"install method": "npm",
"platform": "macos-aarch64",
"version": "0.148.0"
}
}
}
Evidence
- The cached access token was issued at
2026-08-20T03:52:43Z, about five minutes before the failure. - Its JWT audience was
https://api.openai.com/v1. - Its expiry was
2026-08-30T03:52:43Z, so it was not near expiry. - The credential file timestamp,
last_refresh, and access-token SHA-256 fingerprint did not change between the failed run and the successful controls. - At
2026-08-20T04:03:49Z, an isolatedgpt-5.6-lunarequest using a byte-for-byte copy of the same credential returnedOKand exited 0. - At
2026-08-20T04:09:55Z, an isolated request using the incident's--search,exec --json, file-auth, andmaxreasoning options returnedOK, emitted normal thread and turn events, and had no 401 on stderr. - A complete downstream smoke run using the same ChatGPT login also produced a schema-valid Codex result.
codex login statuscontinued to reportLogged in using ChatGPT.- A search of nearby local sessions found no second independent occurrence of this WebSocket 401 signature.
- Codex 0.148.0 source maps a failed HTTP WebSocket upgrade to
TransportError::Http, has ChatGPT unauthorized-recovery steps, and uses five response-stream retries by default. One warmup handshake plus the initial request and five retries is consistent with the seven logged failures.
Impact
The transient 401 aborted an automated code-review subprocess before it could return a structured result. Callers correctly saw a nonzero exit, but the job lost its review result despite credentials that worked unchanged a few minutes later.
This is especially disruptive for CI and local automation because a user cannot interactively repair the session, and the saved error does not identify whether Codex exhausted credential reload, token refresh, or transport recovery.
Additional context
Related reports appear to cover different auth states:
#30514starts with an emptyCODEX_HOMEand no credentials.#30351is a persistentapi.responses.writescope failure.#37192changes auth mode to API key and sends a literaldummykey after a network change.openclaw/agent-skills#30covered an AutoReview isolation bug that dropped auth-only config. That issue is closed, and the current helper reconstructs auth-only config and stages file-backed credentials.
In this occurrence, auth mode remained ChatGPT, no API-key environment variable or proxy was present, the helper staged the credential successfully, and later direct Codex requests accepted the same credential. That is why this report targets Codex's transient 401 recovery path rather than the wrapper.
The original local rollout is retained, but its thread identifier and credential material are intentionally omitted from this public draft.