Multi-account switcher for the OpenAI Codex CLI — auto-picks the ChatGPT Pro account with the lowest usage.
Project description
codex-swap
Pick the ChatGPT Pro account with the lowest usage every time you launch Codex CLI. One command, no thinking.
$ cx
cx: using slot 2 (alex@example.com, 5h 4%, 7d 12%)
╭─── OpenAI Codex (v0.128.0) ───╮
│ model: gpt-5.5 fast │
╰────────────────────────────────╯
If you have multiple Codex Pro accounts and you keep hitting the 5-hour cap on whichever one you happened to be logged into, this is for you. It's the Codex equivalent of cswap for Claude Code.
What it does
- Saves a snapshot of each account's
~/.codex/auth.jsoninto its own slot. - Keeps a durable per-slot usage store (5-hour + 7-day windows) and decays each window automatically when the API's
resets_attime passes — so the picker stays accurate without re-measuring. - On each
cxlaunch, does a cheap rollout scan and merges any fresh findings into that store. Slots not seen in this scan keep their previous record. - Swaps
auth.jsonto the slot with the lowest usage, thenexecscodex. - Snapshots back any refreshed tokens so the rotation chain never breaks.
codex-swap seedpopulates usage for every slot in parallel via isolatedCODEX_HOMEs — one-time setup; no re-seeding on every launch.
Install
Recommended — installs two commands (codex-swap and cx) into ~/.local/bin, using PyPI when available and falling back to the latest GitHub release. It uses uv when available, then pipx, then a private Python venv:
curl -fsSL https://github.com/aneym/codex-swap/releases/latest/download/install.sh | bash
Or install directly from PyPI with your preferred isolated Python tool manager:
uv tool install codex-swap
# or:
pipx install codex-swap
If PyPI is unavailable, install the latest GitHub release wheel directly:
uv tool install https://github.com/aneym/codex-swap/releases/download/v0.1.3/codex_swap-0.1.3-py3-none-any.whl
# or:
pipx install https://github.com/aneym/codex-swap/releases/download/v0.1.3/codex_swap-0.1.3-py3-none-any.whl
Make sure ~/.local/bin is on your PATH. If not:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
exec zsh
Verify:
codex-swap --version
cx --help
More install, upgrade, and shell-helper options live in docs/INSTALL.md.
First-time setup
You need to be logged into Codex with one of your ChatGPT Pro accounts already (codex login). Then:
# 1. Save the account you're already logged into as slot 1.
codex-swap add
# 2. Add the rest of your accounts. codex-swap will pop a browser per account.
# onboard auto-seeds usage for every slot at the end (parallel ~50 tokens
# per slot), so `cx` has informed picks from the very first launch.
codex-swap onboard 2
# 3. Confirm everything works.
codex-swap verify
Output of verify should be all ok (or rate_limited if a window is currently capped — that's fine, auth is still healthy). If a row says error, the probe failed for a local Codex/model/config reason rather than a dead refresh token.
If you imported slots manually (e.g. via add / import-profile) and skipped onboard, run the seed step yourself once:
codex-swap seed # probes any slot without a usage record, in parallel
codex-swap seed --all # re-seed every slot (re-measure)
If you already have Codex profile directories, import them instead of logging in again:
codex-swap import-profile personal
codex-swap import-profile work --label work
import-profile accepts a profile name under ~/.codex-profiles, a profile directory, or an auth.json path. It supports both ChatGPT OAuth profiles and API-key profiles; API-key profiles do not expose 5-hour/7-day ChatGPT usage telemetry, so they show as unknown usage and are best pinned explicitly when needed.
Daily use
Replace codex with cx in your workflow:
cx # auto-pick lowest-usage slot, then run codex
cx exec "fix bug" # all codex args are forwarded
That's the whole interface.
When something goes wrong
If a slot's refresh token dies (you ran codex logout somewhere, the token aged out, etc.), cx will still try to use it and codex will print "refresh token was already used" or similar. Fix everything in one shot:
codex-swap reconnect # finds dead slots, walks you through fresh logins
If you want to fix one specific slot:
codex-swap reauth 1 # opens browser, log in to that slot's account
All commands
| Command | What it does |
|---|---|
cx |
Auto-pick lowest-usage slot and exec codex (this is what you'll use) |
codex-swap add |
Save the currently logged-in account as a new slot |
codex-swap import-profile <profile> |
Save an existing ~/.codex-profiles/<profile>/auth.json or auth file as a slot |
codex-swap remove <slot> |
Remove a slot |
codex-swap list |
Show all slots with usage % |
codex-swap status |
Show which slot is active right now |
codex-swap switch [<slot>] |
Switch to a slot (no arg → rotate to next) |
codex-swap reauth <slot> |
Re-mint a slot via fresh codex login |
codex-swap reconnect |
Verify all slots, then reauth every broken one |
codex-swap onboard [N] |
Guided login for N accounts in a row |
codex-swap verify |
Test every slot with a real codex exec call |
codex-swap usage |
Refresh & print the per-slot usage cache (decay-aware) |
codex-swap seed [<slot>...] |
Probe slots in parallel (isolated CODEX_HOME) to populate usage data |
codex-swap stash |
Snapshot live auth.json back into its slot |
codex-swap purge --yes |
Delete all codex-swap state |
<slot> accepts a slot number, an email, or an account_id.
Pinning and bypass
CXSWAP_SLOT=2 cx # force a specific slot for this run
CXSWAP_SKIP_AUTO=1 cx # skip auto-pick (use whatever's currently in auth.json)
The explicit names CODEX_SWAP_SLOT and CODEX_SWAP_SKIP_AUTO work too.
Optional shell aliases
If you like shorter names, add to your ~/.zshrc:
cxraw() { CXSWAP_SKIP_AUTO=1 cx "$@"; }
cxslot() { CXSWAP_SLOT="$1" cx "${@:2}"; }
cxaccounts() { codex-swap list "$@"; }
cxstatus() { codex-swap status "$@"; }
cxverify() { codex-swap verify "$@"; }
cxreauth() { codex-swap reauth "$@"; }
cxreconnect() { codex-swap reconnect "$@"; }
Reload with exec zsh.
How "lowest usage" is computed
Codex persists a token_count event with rate_limits.primary (5-hour window) and rate_limits.secondary (7-day window) into ~/.codex/sessions/**/*.jsonl after every turn. codex-swap correlates conversation IDs to account IDs via ~/.codex/logs_2.sqlite (user.account_id="..." + conversation.id=... in the otel log bodies) to figure out which slot owns each rollout.
The persisted store at ~/.codex-swap/cache/usage.json is durable: each rescan merges new findings (per-slot, higher scanned_at wins) instead of overwriting. Slots not seen in a given scan keep their previous record. When a window's resets_at time passes, codex-swap reports that window as 0% via decay — the picker doesn't need to re-measure to notice the reset.
Slots with no record at all (e.g. freshly added, never used here) get usage data via codex-swap seed, which probes each named slot in parallel using its own temporary CODEX_HOME (no contention with the live ~/.codex/auth.json). onboard runs this automatically; otherwise call it manually once.
The picker sorts in three buckets, lower wins:
- Known + healthy (both windows below 80% after decay)
- Unknown (no record yet — assumed fresh; bias toward learning)
- Known + at/above 80% on either window (last resort)
Within a bucket, ties break on (5h%, 7d%, slot#).
Critical: never run codex logout
codex logout calls a server-side revoke that invalidates the refresh token at the OAuth provider. Every other slot whose snapshot pre-dates that revoke is then permanently dead. codex-swap's onboarding and reauth flows use rm ~/.codex/auth.json instead — same effect locally, no server-side blast radius.
If a slot ever goes bad (you ran codex logout by hand, or the token aged out), codex-swap reconnect will detect and fix it.
Refresh token rotation, briefly
ChatGPT issues single-use refresh tokens that rotate on every successful refresh. codex-swap snapshots the live auth.json back into its slot before every swap-out, so the slot's snapshot always carries the latest rotated token. You only get into trouble if you run codex twice on the same slot in parallel — both refresh independently, one of them ends up with a token the server has already burned.
Safe: sequential cx runs, even across many accounts.
Unsafe: two terminals running cx against the same slot at the same time.
Files
~/.codex-swap/
├── accounts/<N>/auth.json # per-slot snapshots (chmod 600)
├── sequence.json # slot order + email/account_id metadata
├── state.json # last switched slot + timestamp
└── cache/usage.json # durable per-slot usage records (with resets_at decay)
~/.codex/auth.json is the live file Codex reads. codex-swap only ever swaps that one file in and out.
Caveats
- macOS only right now.
- Requires
codexv0.122 or newer (the rollout schema withrate_limits.primary/secondarywas added around that time). If multiple Codex installs exist onPATH, codex-swap chooses the newest detected binary. OPENAI_API_KEYin your environment will overrideauth.json. codex-swap unsets it when runningcodex loginso the OAuth path wins; if you wantcxto do the same for the launched session, wrap it:alias cx='env -u OPENAI_API_KEY codex-swap launch'.
Uninstall
codex-swap purge --yes # before uninstall, removes ~/.codex-swap
uv tool uninstall codex-swap # or: pipx uninstall codex-swap
Development
git clone https://github.com/aneym/codex-swap
cd codex-swap
bash scripts/install-dev.sh # installs editable via uv tool
pytest -q # run the unit tests
ruff check . # lint
Releasing
Maintainer flow:
# 1. Move bullet points into the [Unreleased] block of CHANGELOG.md
# 2. Cut the release:
scripts/release.sh 0.1.1
The script runs the full packaging gate, bumps src/codex_swap/__init__.py, promotes [Unreleased] into a versioned section, tags v0.1.1, and pushes. A successful main CI run also auto-creates a missing version tag, so release publication stays automated even when maintainers only push the release commit. The publish.yml workflow builds, checks, uploads to PyPI via Trusted Publishing, and creates a GitHub release with the changelog notes attached. No long-lived API tokens involved.
See docs/RELEASE.md for the exact automation path.
One-time PyPI Trusted Publisher setup
Before the first publish runs cleanly:
- In PyPI's pending publisher flow, create the
codex-swapproject publisher: https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/ - Configure the GitHub publisher fields exactly:
- PyPI project name:
codex-swap - Owner:
aneym - Repository:
codex-swap - Workflow:
publish.yml - Environment:
pypi
- PyPI project name:
- On GitHub → repo Settings → Environments → confirm
pypiexists (no secrets needed — the OIDC token handles auth). - Rerun the failed
Publish to PyPIjob, or push the next version tag.
After that, every git push of a vX.Y.Z tag publishes automatically.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file codex_swap-0.1.4.tar.gz.
File metadata
- Download URL: codex_swap-0.1.4.tar.gz
- Upload date:
- Size: 48.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9c3896b6b44b64f82d2d1c2fcd36b577169617aa5d5ad1ffb7e0b01d06382a1
|
|
| MD5 |
22cd27b7159442abee7c3d8537e6a590
|
|
| BLAKE2b-256 |
fe6ed42edcb620f6f95dbadb03dc8cc9673f245bddc68f19ef37356ecabe917f
|
Provenance
The following attestation bundles were made for codex_swap-0.1.4.tar.gz:
Publisher:
publish.yml on aneym/codex-swap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
codex_swap-0.1.4.tar.gz -
Subject digest:
a9c3896b6b44b64f82d2d1c2fcd36b577169617aa5d5ad1ffb7e0b01d06382a1 - Sigstore transparency entry: 1461977371
- Sigstore integration time:
-
Permalink:
aneym/codex-swap@0b11faa9eb62501cf3f21437141b274b8fb5eb2b -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/aneym
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0b11faa9eb62501cf3f21437141b274b8fb5eb2b -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file codex_swap-0.1.4-py3-none-any.whl.
File metadata
- Download URL: codex_swap-0.1.4-py3-none-any.whl
- Upload date:
- Size: 29.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7950d82d1404a99c14473f806a0ab52e6035866ff20080ff21762dc490fdcd70
|
|
| MD5 |
250dd99cb9088a399c4c08eaf159e6e0
|
|
| BLAKE2b-256 |
a3462cf02d13447dc2deb896effc2f78c71f3ae8f90f8a1519b61f790f77cf19
|
Provenance
The following attestation bundles were made for codex_swap-0.1.4-py3-none-any.whl:
Publisher:
publish.yml on aneym/codex-swap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
codex_swap-0.1.4-py3-none-any.whl -
Subject digest:
7950d82d1404a99c14473f806a0ab52e6035866ff20080ff21762dc490fdcd70 - Sigstore transparency entry: 1461977389
- Sigstore integration time:
-
Permalink:
aneym/codex-swap@0b11faa9eb62501cf3f21437141b274b8fb5eb2b -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/aneym
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0b11faa9eb62501cf3f21437141b274b8fb5eb2b -
Trigger Event:
workflow_dispatch
-
Statement type: