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 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.
  • Before each cx launch, scans your local Codex rollouts to learn each account's primary (5-hour) and secondary (7-day) usage percent.
  • Swaps auth.json to the slot with the lowest usage, then execs codex.
  • Snapshots back any refreshed tokens so the rotation chain never breaks.

Install

Recommended — installs two commands (codex-swap and cx) into ~/.local/bin, using PyPI when available and falling back to GitHub:

curl -fsSL https://raw.githubusercontent.com/aneym/codex-swap/main/scripts/install.sh | bash

Or pick a package manager directly:

# Recommended (faster, isolated):
uv tool install codex-swap

# or:
pipx install codex-swap

Until codex-swap is on PyPI, install from this repo:

uv tool install git+https://github.com/aneym/codex-swap
# or:
pipx install git+https://github.com/aneym/codex-swap

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.
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 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
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

After every Codex turn, the CLI persists a token_count event with rate_limits.primary (5-hour window) and rate_limits.secondary (7-day window) into ~/.codex/sessions/**/*.jsonl. codex-swap correlates conversation IDs to account IDs via ~/.codex/logs_2.sqlite (user.account_id="..." + conversation.id=... in the otel log bodies), then for each managed slot pulls the latest snapshot from a rollout owned by that slot. The picker sorts by (5h%, 7d%, slot#).

A slot with no usage data sorts as if it were 101% — it'll be picked only after the others have logged usage.

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         # cached rate-limit snapshots

~/.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

uv tool uninstall codex-swap        # or: pipx uninstall codex-swap
codex-swap purge --yes              # before uninstall, removes ~/.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. Reserve the package name on PyPI by uploading any 0.0.x build manually (or have a maintainer claim it).
  2. On PyPI → Manage projectPublishing → add a GitHub publisher:
    • Owner: aneym
    • Repository: codex-swap
    • Workflow: publish.yml
    • Environment: pypi
  3. On GitHub → repo SettingsEnvironments → create pypi (no secrets needed — the OIDC token handles auth).

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.1.tar.gz (33.0 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.1-py3-none-any.whl (22.3 kB view details)

Uploaded Python 3

File details

Details for the file codex_swap-0.1.1.tar.gz.

File metadata

  • Download URL: codex_swap-0.1.1.tar.gz
  • Upload date:
  • Size: 33.0 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.1.tar.gz
Algorithm Hash digest
SHA256 fbbd41b4489b0a76f4eea8e4f403ee208380da37fa908d2655b4587720a3848b
MD5 89aecb1c5f8a2a427d3be1756f60f236
BLAKE2b-256 1c54a3572633c6cb9faca938ce857545315a82ee4d6c7e5fe942429583e7fc54

See more details on using hashes here.

Provenance

The following attestation bundles were made for codex_swap-0.1.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: codex_swap-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 22.3 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b54d2500187d812cc7c9b7c4956fdc6fef4ffb7021d37cf9d8bc274dd3e97fb1
MD5 b67f9c71f72be21b0025792bc08ab399
BLAKE2b-256 16355379a44554dc9d69aa0cefd187199f417a7fc629e140dd56a05eef4bdf2d

See more details on using hashes here.

Provenance

The following attestation bundles were made for codex_swap-0.1.1-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