Skip to main content

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 Settings → Environments → 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

Metadata

Release files for codex-swap 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for codex-swap 0.1.5
File Size Uploaded
codex_swap-0.1.5.tar.gz 50.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for codex-swap 0.1.5
File Interpreter ABI Platform
codex_swap-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 80.7 kB

Release files / codex_swap-0.1.5.tar.gz

Download URL codex_swap-0.1.5.tar.gz
Size 50.3 kB
Tags Source
SHA-256 checksum
How to use checksums
a21a8b65b2f475c015fa1f1e91e98ea17e37c978118da6294081650c90053fae
BLAKE2b-256 checksum
How to use checksums
150405bb45638822543d93b138169f231072db3b8df319f91bbf6084eee3c905
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Release files / codex_swap-0.1.5-py3-none-any.whl

Download URL codex_swap-0.1.5-py3-none-any.whl
Size 30.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
34ff510d540011bd7d6e12df4a1c60cf4096044a4f58162e4a45f72ca4616bda
BLAKE2b-256 checksum
How to use checksums
0408bd10685229e560d7915985d9b1a87ce37971078f850eed1a5a9622fba628
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.5 This release

2 release files

0.1.4

2 release files

0.1.2

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page