Skip to main content

Multi-account switcher for the OpenAI Codex CLI — auto-picks the ChatGPT Pro account with the lowest usage.

Project description

codex-swap

CI PyPI Release License: MIT

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.json into 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_at time passes — so the picker stays accurate without re-measuring.
  • On each cx launch, 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.json to the slot with the lowest usage, then execs codex.
  • Snapshots back any refreshed tokens so the rotation chain never breaks.
  • codex-swap seed populates usage for every slot in parallel via isolated CODEX_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:

  1. Known + healthy (both windows below 80% after decay)
  2. Unknown (no record yet — assumed fresh; bias toward learning)
  3. 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 codex v0.122 or newer (the rollout schema with rate_limits.primary/secondary was added around that time). If multiple Codex installs exist on PATH, codex-swap chooses the newest detected binary.
  • OPENAI_API_KEY in your environment will override auth.json. codex-swap unsets it when running codex login so the OAuth path wins; if you want cx to 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:

  1. In PyPI's pending publisher flow, create the codex-swap project publisher: https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/
  2. Configure the GitHub publisher fields exactly:
    • PyPI project name: codex-swap
    • Owner: aneym
    • Repository: codex-swap
    • Workflow: publish.yml
    • Environment: pypi
  3. On GitHub → repo SettingsEnvironments → confirm pypi exists (no secrets needed — the OIDC token handles auth).
  4. Rerun the failed Publish to PyPI job, 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

codex_swap-0.1.4.tar.gz (48.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

codex_swap-0.1.4-py3-none-any.whl (29.5 kB view details)

Uploaded Python 3

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

Hashes for codex_swap-0.1.4.tar.gz
Algorithm Hash digest
SHA256 a9c3896b6b44b64f82d2d1c2fcd36b577169617aa5d5ad1ffb7e0b01d06382a1
MD5 22cd27b7159442abee7c3d8537e6a590
BLAKE2b-256 fe6ed42edcb620f6f95dbadb03dc8cc9673f245bddc68f19ef37356ecabe917f

See more details on using hashes here.

Provenance

The following attestation bundles were made for codex_swap-0.1.4.tar.gz:

Publisher: publish.yml on aneym/codex-swap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for codex_swap-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 7950d82d1404a99c14473f806a0ab52e6035866ff20080ff21762dc490fdcd70
MD5 250dd99cb9088a399c4c08eaf159e6e0
BLAKE2b-256 a3462cf02d13447dc2deb896effc2f78c71f3ae8f90f8a1519b61f790f77cf19

See more details on using hashes here.

Provenance

The following attestation bundles were made for codex_swap-0.1.4-py3-none-any.whl:

Publisher: publish.yml on aneym/codex-swap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page