fix(auth): codex exec aborts on transient Responses WebSocket 401 with valid ChatGPT credentials

Open 💬 0 comments Opened Aug 20, 2026 by coygeek

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.

  1. Sign in with ChatGPT and confirm the CLI sees the cached login:

``sh
codex login status
``

  1. Start a noninteractive run from an isolated CODEX_HOME containing 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" \
-
``

  1. Send a small prompt on stdin.
  1. 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.rs
  • codex-rs/core/src/client.rs
  • codex-rs/core/src/responses_retry.rs
  • codex-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 isolated gpt-5.6-luna request using a byte-for-byte copy of the same credential returned OK and exited 0.
  • At 2026-08-20T04:09:55Z, an isolated request using the incident's --search, exec --json, file-auth, and max reasoning options returned OK, 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 status continued to report Logged 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:

  • #30514 starts with an empty CODEX_HOME and no credentials.
  • #30351 is a persistent api.responses.write scope failure.
  • #37192 changes auth mode to API key and sends a literal dummy key after a network change.
  • openclaw/agent-skills#30 covered 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.

View original on GitHub ↗