Reverse-Engineering Codex Auth and Usage Recovery

A detailed walkthrough of how we figured out where Codex stores ChatGPT auth state, how stale tokens can be recovered, and how the dashboard now reloads or refreshes tokens before giving up.

Back to the Codex overview

Table of Contents

  1. What We Were Trying to Do
  2. Architecture Overview
  3. Phase 1 — Find the Local Auth State
  4. Phase 2 — Prove Refresh Is Possible
  5. Phase 3 — Follow the Installed Codex Binary
  6. Phase 4 — Infer the Refresh Request
  7. Phase 5 — Wire It into the Dashboard
  8. Final Script Explanation
  9. Key Findings Summary

1. What We Were Trying to Do

The original dashboard worked by copying the current access_token out of ~/.codex/auth.json and storing it in Codex/.local/accounts.ini. That is enough for a snapshot, but it creates a weakness:

  1. The copied access token can go stale
  2. Codex itself keeps working, which means it must know how to recover
  3. We wanted to discover that recovery path and reuse it
The key question If the copied token fails, do we just re-copy the newest token from auth.json, or can we actually reproduce Codex's own refresh flow?

2. Architecture Overview

At a high level, the flow we ended up with looks like this:

┌──────────────┐      ┌──────────────────┐      ┌────────────────────┐      ┌─────────────────────────┐
│  Codex CLI    │      │ ~/.codex/auth.json │      │ auth0.openai.com   │      │ chatgpt.com usage API    │
└──────┬───────┘      └─────────┬────────┘      └──────────┬─────────┘      └────────────┬────────────┘
       │                         │                          │                               │
       │ ── read tokens ──────▶ │                          │                               │
       │                         │                          │                               │
       │ access_token          │                          │                               │
       │ account_id             │                          │                               │
       │ refresh_token          │                          │                               │
       │                         │                          │                               │
       │ ────────────────────────────────────────────────────────────────────────────▶ │
       │ GET /backend-api/wham/usage                                                           │
       │ Authorization: Bearer {access_token}                                              │
       │ ChatGPT-Account-Id: {account_id}                                               │
       │                         │                          │                               │
       │ ◀──────────────────────────────────────────────────────────────────────────── │
       │                         │                          │                               │
       │ [if token expired]     │                          │                               │
       │                         │                          │                               │
       │ ────────────────────────▶ │                          │                               │
       │ load refresh_token     │                          │                               │
       │                         │                          │                               │
       │ ────────────────────────────────────────────▶ │                               │
       │ POST /oauth/token                                    │                               │
       │ grant_type=refresh_token                            │                               │
       │ client_id=app_EMoamEEZ73f0CkXaXp7hrann               │                               │
       │ ◀──────────────────────────────────────────── │                               │
       │ {access_token, refresh_token?, id_token?}        │                               │
       │                         │                          │                               │
       │ ── write updated auth ─▶ │                          │                               │
       │                         │                          │                               │
       │ ────────────────────────────────────────────────────────────────────────────▶ │
       │ retry GET /backend-api/wham/usage
Important distinction The dashboard is still a ChatGPT-account usage tool, not an OpenAI API-key tool. The auth here is ChatGPT login state stored by Codex, not a permanent API key.

3. Phase 1 — Find the Local Auth State

Step 1.1: Inspect the local Codex directory Useful

The first clue was simple: Codex already stores its own state under ~/.codex/.

Command & Output
$ ls -la ~/.codex
...
-rw-------  auth.json
drwxr-xr-x  sessions
...

This immediately suggested that auth.json was the primary source of truth and sessions/ was only useful for the optional local activity counts.

Step 1.2: Inspect auth.json structure Found It

Instead of printing raw secrets, we inspected the shape of the file:

Command & Output
$ python3 - <<'PY'
import json, pathlib
p = pathlib.Path.home()/'.codex'/'auth.json'
obj = json.loads(p.read_text())
print(sorted(obj.keys()))
print(sorted(obj.get('tokens', {}).keys()))
PY

['OPENAI_API_KEY', 'auth_mode', 'last_refresh', 'tokens']
['access_token', 'account_id', 'id_token', 'refresh_token']

This was the first big breakthrough: Codex is not just caching an access token. It also stores a refresh_token.

Key Finding #1 ~/.codex/auth.json already contains everything needed for recovery: access_token, refresh_token, id_token, and account_id.

4. Phase 2 — Prove Refresh Is Possible

Step 2.1: Decode the token claims Critical Clue

If the access token includes offline_access, that is a very strong signal that a refresh flow exists.

Command & Output
$ python3 - <<'PY'
import json, pathlib, base64
p = pathlib.Path.home()/'.codex'/'auth.json'
tok = json.loads(p.read_text())['tokens']['access_token']
payload = tok.split('.')[1] + '=' * (-len(tok.split('.')[1]) % 4)
data = json.loads(base64.urlsafe_b64decode(payload))
print(data['iss'])
print(data['scp'])
PY

https://auth.openai.com
['openid', 'profile', 'email', 'offline_access', 'api.connectors.read', 'api.connectors.invoke']

That tells us two things:

  1. The issuer is https://auth.openai.com
  2. The token was granted offline_access, which is the classic OAuth signal for refresh support

Step 2.2: Understand the current weakness Design Gap

The dashboard originally copied the access token into Codex/.local/accounts.ini. That works, but only until the copied token becomes stale.

Command & Output
# Original bootstrap behavior
$ jq -r '.tokens.access_token' ~/.codex/auth.json
$ jq -r '.tokens.account_id' ~/.codex/auth.json
# ...then write those values into Codex/.local/accounts.ini

This is fine for a snapshot, but it means the dashboard can lag behind whatever Codex itself is doing.

5. Phase 3 — Follow the Installed Codex Binary

Step 3.1: Inspect the obvious entrypoint Partial Dead End

The first thing we opened was the Homebrew launcher:

Command & Output
$ which codex
/opt/homebrew/bin/codex

$ readlink -f $(which codex)
/opt/homebrew/lib/node_modules/@openai/codex/bin/codex.js

$ sed -n '1,40p' /opt/homebrew/lib/node_modules/@openai/codex/bin/codex.js
#!/usr/bin/env node
// Unified entry point for the Codex CLI.
...
const binaryPath = ...
spawn(binaryPath, process.argv.slice(2), ...)

This told us the JS file is just a launcher. The real auth logic lives in the native platform binary it spawns.

Step 3.2: First guess the binary path Wrong Path

An early assumption was that the platform package would be installed as a top-level sibling npm package.

Command & Output
$ find /opt/homebrew/lib/node_modules/@openai/codex-darwin-arm64 -maxdepth 4 -type f
find: ... No such file or directory

That was wrong on this machine. The platform bundle was nested under the main package's own node_modules.

Step 3.3: Find the real platform bundle Found It

Command & Output
$ find /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai -maxdepth 4 -type d | sort
...
/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64
/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor
/opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex
...

Step 3.4: Search the binary strings Critical Discovery

Once we had the native binary path, we searched for auth-related clues:

Command & Output
$ strings /opt/homebrew/lib/node_modules/@openai/codex/node_modules/@openai/codex-darwin-arm64/vendor/aarch64-apple-darwin/codex/codex \
  | rg "refresh_token|grant_type|code_verifier|oauth token exchange|app_EMoamEEZ73f0CkXaXp7hrann|chatgpt_account_id"

Access token expired, refreshing.
Refreshed access token.
starting oauth token exchange
oauth token exchange succeeded
access_token
refresh_token
grant_type
code_verifier
client_id
app_EMoamEEZ73f0CkXaXp7hrann
chatgpt_account_id

This was enough to move from “maybe there is refresh” to “Codex definitely has a built-in OAuth token exchange path.”

Key Finding #2 The native Codex binary contains explicit refresh-related strings, including the refresh token field name, form-encoded OAuth request parts, a client id, and log text that says the token is refreshed when it expires.

6. Phase 4 — Infer the Refresh Request

Step 4.1: Connect the issuer to OIDC discovery Confirmed

From the token claims we had iss=https://auth.openai.com. That makes the next step standard OAuth/OpenID Connect discovery:

Command & Output
$ curl -s https://auth.openai.com/.well-known/openid-configuration | jq -r '.token_endpoint'
https://auth0.openai.com/oauth/token

That gives us the missing host for the refresh request.

Step 4.2: Infer the minimal refresh body Working Shape

Between the binary strings and the discovery document, the request shape becomes fairly clear:

POST https://auth0.openai.com/oauth/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json

grant_type=refresh_token
refresh_token=<refresh_token from ~/.codex/auth.json>
client_id=app_EMoamEEZ73f0CkXaXp7hrann

That matches everything we observed locally:

ClueWhat it told us
offline_access in token scopeRefresh support should exist
iss=https://auth.openai.comOIDC discovery starts from auth.openai.com
Binary strings: refresh_token, grant_type, client_idStandard form-encoded OAuth token exchange
Binary string: app_EMoamEEZ73f0CkXaXp7hrannThe ChatGPT client id Codex is using
Discovery documentConfirmed https://auth0.openai.com/oauth/token
What we did not fully prove inside this sandbox We did not execute a live refresh request from the sandboxed terminal because outbound DNS/network is blocked here. The request shape is based on local binary evidence plus OIDC discovery, and the standalone helper script reflects that shape exactly.

7. Phase 5 — Wire It into the Dashboard

Step 5.1: First recovery attempt — reload auth.json Cheap Recovery

If the copied token is stale because Codex already refreshed it in the background, we do not need to hit the network at all. We can just reload the latest token from auth.json.

# Recovery order inside codex_nerve.py
1. usage request fails with 401/403
2. reload ~/.codex/auth.json
3. if access_token changed, retry usage request
4. only if that still fails, do a refresh_token exchange

Step 5.2: Second recovery attempt — refresh and retry Full Recovery

When reload is not enough, the dashboard now performs the refresh exchange, writes the new token state back to auth.json, and retries the usage API.

Relevant code paths
Codex/codex_nerve.py
load_local_auth_tokens(...)
reload_account_from_auth_file(...)
refresh_access_token_from_auth_file(...)
refresh_account(...)

8. Final Script Explanation

The dashboard

codex_nerve.py is now responsible for three auth behaviors:

  1. Use the copied snapshot token for the normal fast path
  2. Reload from auth_file if the snapshot goes stale
  3. Refresh from the stored refresh_token if reload is not enough

The standalone helper

codex_refresh_auth.py is the smallest runnable example of the inferred Codex refresh flow. It does two things:

Sample commands
# Show the inferred request shape without calling the network
$ python3 Codex/codex_refresh_auth.py --print-only

# Perform the refresh and write updated tokens back to ~/.codex/auth.json
$ python3 Codex/codex_refresh_auth.py

The helper intentionally updates ~/.codex/auth.json because that is the same local state Codex itself relies on.

9. Key Findings Summary

What we learned Codex stores ChatGPT auth state in ~/.codex/auth.json, including a refresh_token. The access token carries offline_access, the binary contains explicit OAuth refresh clues plus the ChatGPT client id, and OpenID discovery leads to https://auth0.openai.com/oauth/token. That was enough to add automatic recovery to the dashboard and produce a standalone refresh helper.