| name | codex-oauth-ci |
| description | Use when setting up or fixing Codex (ChatGPT OAuth) authentication for GitHub Actions across one or many repos — e.g. CI Codex jobs failing with "401 token_expired" or "refresh token was already used", seeding/rotating the CODEX_AUTH_JSON secret, or onboarding a new repo to Codex CI. |
Codex OAuth auth for CI (across many repos)
The problem
Codex CI authenticates with a ChatGPT OAuth login stored in each repo's
CODEX_AUTH_JSON secret. OpenAI rotates refresh tokens single-use: the moment
any copy of a login refreshes, every other copy holding the old refresh token dies
→ 401 token_expired / "refresh token was already used". So one login shared across
many repos + your laptop self-destructs over time.
The model (shared login + scheduled re-seed)
- Keep one CI-dedicated login in its own
CODEX_HOME (~/.codex-ci), never
used for local work, so nothing rotates it out from under CI.
scripts/seed_codex_auth.py pushes that login's auth.json into the
CODEX_AUTH_JSON secret of every repo in config/codex_auth_repos.txt.
- Re-seed on a cadence shorter than the ~10-day access-token life so CI never
refreshes — and therefore never rotates the shared token.
- Each target repo's Codex workflow must run with
persist_auth: false (so a CI
run can't rotate/persist over the shared secret).
One-time setup
mkdir -p ~/.codex-ci && chmod 700 ~/.codex-ci
CODEX_HOME=~/.codex-ci codex login
$EDITOR config/codex_auth_repos.txt
python scripts/seed_codex_auth.py --dry-run
python scripts/seed_codex_auth.py
In each target repo, set persist_auth: "false" in the Codex workflow
(.github/workflows/codex-code-review.yml or equivalent). With model A the workflow
must NOT persist refreshed tokens back to the secret.
Routine re-seed
Run the seeder on a schedule (it must run where the live ~/.codex-ci login is —
your machine, not a GitHub Action). It refreshes the central chain in place
(codex login status) and re-pushes, so the login never has to be recreated:
python scripts/seed_codex_auth.py
Example launchd agent (~/Library/LaunchAgents/dev.mungall.codex-seed.plist),
running every 2 days — comfortably under the 10-day token life:
<plist version="1.0"><dict>
<key>Label</key><string>dev.mungall.codex-seed</string>
<key>ProgramArguments</key><array>
<string>/bin/zsh</string><string>-lc</string>
<string>cd ~/repos/agent-watcher && python scripts/seed_codex_auth.py >> ~/.codex-ci/seed.log 2>&1</string>
</array>
<key>StartInterval</key><integer>172800</integer>
</dict></plist>
launchctl load ~/Library/LaunchAgents/dev.mungall.codex-seed.plist
Onboarding a new repo
- Add
owner/repo to config/codex_auth_repos.txt.
- Set
persist_auth: "false" in that repo's Codex workflow.
python scripts/seed_codex_auth.py (seeds the new repo along with the rest).
Gotchas
- Don't use the
~/.codex-ci login locally. Local Codex use rotates the chain
and breaks the CI copy. Keep your normal ~/.codex for local work.
- Expired token → the seeder refuses rather than fan a dead token out to N repos.
Fix:
CODEX_HOME=~/.codex-ci codex login again, then re-seed.
- Per-repo failures (
ERROR ... 403) usually mean you lack admin on that repo;
the seeder skips it and continues.
- Adding Codex CI to a brand-new repo still needs
CODEX_AUTH_JSON to exist there —
the seeder creates/overwrites it via gh secret set, which requires repo admin.