Tiny macOS 14+ menu bar app that manages multiple OpenAI Codex accounts and shows per-profile usage. Switch profiles without logging in again, track 5-hour and weekly limits with reset countdowns. Auth stored in macOS Keychain. No Dock icon, no main window, just a menu bar dropdown.
- You have multiple Codex accounts (personal, work, client). Logging out and back in every time is painful. This app makes switching instant.
- Usage limits reset on rolling windows, and you can't see them for inactive accounts. This app tracks every saved profile, not just the active one.
- Auth lives in macOS Keychain. No telemetry, no cloud sync, no account linking.
- Manage multiple saved Codex profiles with custom labels
- See 5-hour and weekly usage for each saved profile
- Show credit balance when Codex exposes it
- Switch accounts without logging in every time
- Refresh inactive OAuth profiles so usage data stays current
- Keep the last known usage snapshot in the menu
- Launch at login
- Copy redacted debug info, open the log file, and jump to the GitHub issue form
The app reads and writes macOS Keychain items it creates, ~/.codex/auth.json, and its own config at ~/.codex-switcher/config.json. It does not read browser data, does not access files outside those paths, and does not send telemetry or phone home. All data stays on your machine.
Requires macOS 14+ and the Codex desktop app.
Download the latest signed and notarized DMG from
GitHub Releases, open
it, and drag CodexProfileSwitcher.app to Applications.
Install via the 4lau/tap:
brew install --cask 4lau/tap/codex-profile-switcherBuilding from source requires Xcode.app. The test and package scripts use SwiftPM targets under Sources/ and set DEVELOPER_DIR=/Applications/Xcode.app when available.
git clone https://github.com/4LAU/codex-profile-switcher.git
cd codex-profile-switcher
./build.sh
# Start the app
codex-profile-switcherThe menu bar app is installed to ~/.local/bin/codex-profile-switcher, and the
matching Swift CLI helper is installed to ~/.local/bin/codex-profile.
No Keychain prompts for source builds. Keychain item ACLs trust the
binary's code-signing identity. The DMG and Homebrew builds are Developer
ID-signed, so the one-time "Always Allow" per profile holds across updates.
Self-built binaries are ad-hoc signed — every rebuild looks like a new program
to macOS — so they never touch the real Keychain at all: they automatically use
a separate file-based vault at ~/.codex-switcher/dev-auth-store (0600 files,
the same protection Codex itself uses for ~/.codex/auth.json). No Apple
account or certificate is needed, and no prompts appear. To build a CLI that
shares the real Keychain profiles, sign it with any Apple certificate:
make install-cli APP_IDENTITY="Apple Development: you@example.com (TEAMID)"If Codex is not installed at /Applications/Codex.app, set CODEX_APP or
CODEX_CLI before using the helper.
To build a signed app bundle instead of loose binaries:
Scripts/package_app.sh
# Optional Developer ID / Apple Development identity.
APP_IDENTITY="Developer ID Application: Your Name (TEAMID)" Scripts/package_app.shMaintainer releases should use the DMG release flow in docs/RELEASING.md.
Each profile needs one login before the app can switch to it.
Open Settings from the menu bar icon, select a profile, and click Set Up. This opens Codex's normal browser login flow and saves the resulting auth in macOS Keychain. After that, switching does not require logging in again.
You can also set up profiles from the terminal:
codex-profile login 1
codex-profile login 2The codex-profile helper manages profiles from the terminal.
codex-profile app <profile> [workspace]
codex-profile login <profile> [codex-login-args...]
codex-profile status [profile] [--json]
codex-profile list [--json]
codex-profile path <profile>
codex-profile doctor
codex-profile keychain-repair
codex-profile best-auth --dir <path> [--exclude <id1,id2,...>] [--json] [--non-interactive] [--timeout <seconds>]
codex-profile exec [--max-attempts <n>] [--exclude <id1,id2,...>] [--timeout <seconds>] -- <command> [args...]
codex-profile import-auth --dir <path> --profile <id> [--non-interactive] [--timeout <seconds>]
codex-profile lease begin [--exclude <id1,id2,...>] [--ttl <seconds>] [--timeout <seconds>] [--json] [--non-interactive]
codex-profile lease swap <token> [--exclude <id1,id2,...>] [--ttl <seconds>] [--timeout <seconds>] [--json] [--non-interactive]
codex-profile lease end <token> [--profile <id>] [--timeout <seconds>] [--non-interactive]
codex-profile lease gc
Core commands
login— runs an isolated Codex login and saves the resulting auth to the Keychain.app— switches to a profile and relaunches Codex Desktop.status— shows auth state for one or all profiles.--jsonemits a JSON array.list— lists known profiles.--jsonemits a JSON array.path— prints the Keychain location for a profile.doctor— prints environment, installed Codex binaries, auth backend, and profile status.keychain-repair— rewrites saved auth items with current Keychain access settings. Run once if repeated prompts appear after upgrading.
Selects the profile with the most remaining quota and writes its credentials to --dir. Designed for scripted account rotation with codex exec --ephemeral.
Fetches live usage via codex app-server (bounded concurrency of 3) before ranking. Falls back to each profile's cached snapshot when a live fetch fails. Stores fresh snapshots back to the cache.
Non-interactive behavior. When stdin is not a terminal (CI, command substitution, cron), or when --non-interactive is passed, Keychain reads that would show a modal consent prompt are skipped instead of blocking. A global watchdog exits the process after --timeout seconds (default 30) to guarantee termination.
Basic usage — bare profile ID on stdout:
# Use in command substitution
PROFILE=$(codex-profile best-auth --dir /tmp/codex-session)
codex exec --ephemeral --dir /tmp/codex-session -- your-commandJSON output:
codex-profile best-auth --dir /tmp/codex-session --json{"candidates":[{"id":"personal","score":21,"snapshotAgeSeconds":47,"tier":"preferred"},{"id":"work","score":18,"snapshotAgeSeconds":14,"tier":"preferred"}],"fetched":true,"score":18,"selected":"work","tier":"preferred"}Excluding profiles:
# Exclude a profile known to be rate-limited
codex-profile best-auth --dir /tmp/codex-session --exclude workExit codes:
| Code | Meaning |
|---|---|
| 0 | Profile selected and credentials written |
| 1 | Generic failure |
| 2 | No eligible profile (all excluded or exhausted) |
| 3 | No profiles configured |
| 4 | Usage data unavailable (no live fetch succeeded, no cached snapshots) |
| 6 | Keychain interaction required — run codex-profile best-auth once from a terminal to grant access |
| 7 | Watchdog timeout |
Runs any command with CODEX_HOME pointed at the best profile's credentials, with automatic rotation on usage limits. This is the one-line replacement for hand-rolled best-auth / mark-exhausted / import-auth loops:
codex-profile exec -- codex exec --ephemeral -C "$(pwd)" - < prompt.mdPer attempt (up to --max-attempts, default 3):
- Selects the profile with the most remaining quota (same logic and exit codes as
best-auth) into a private temp directory. - Runs the command with
CODEX_HOMEpointing there. stdin and stdout pass through untouched; stderr passes through and is also scanned. - On success, writes refreshed tokens back to the profile (identity-guarded) and exits 0.
- If the command failed and its stderr matches a usage-limit error (
rate limit,usage limit,429,quota exceeded,too many requests), the profile is marked exhausted for an hour and the command retries on the next best profile. Any other failure exits immediately with the child's exit code. Detection scans stderr only — a limit message printed exclusively to stdout is not detected, because stdout streams through verbatim.
The live ~/.codex is never touched, so a running Codex Desktop/CLI session is unaffected. Selection failures use the best-auth exit codes (2/3/4/6); a selection watchdog (--timeout, default 60s) covers only the selection phase, never the wrapped command.
Writes a refreshed auth.json from --dir back to the stored credential for --profile. Intended as the write-back half of a best-auth rotation loop — or just use exec, which does the full loop for you.
Identity guard. Before overwriting, import-auth compares the identity fingerprint of the existing stored credential against the incoming file. If they belong to different accounts the write is refused and the command exits 5. This prevents a credential for one account from silently overwriting a different account's profile.
Headless mode. --non-interactive skips the interactive Keychain-repair step, which can otherwise stall a headless process on a modal consent prompt, and reads the existing credential through the fail-closed vault. --timeout <seconds> arms a watchdog so the call always terminates. This is the write-back path lease end uses.
codex-profile import-auth --dir /tmp/codex-session --profile workExit code 5 means the refreshed credential belongs to a different account. All other failures exit 1.
exec wraps a single command. When you need one Codex session to stay open across many turns (an agent loop, a long review), lease holds an account open and rotates it underneath the session when a limit hits, so the session never restarts and never re-reads the repo.
read -r CHOME TOKEN < <(codex-profile lease begin --json | jq -r '"\(.home) \(.token)"')
export CODEX_HOME="$CHOME"
trap 'codex-profile lease end "$TOKEN"' EXIT
cd "$REPO"
codex exec -C "$REPO" - < prompt.md # first turn reads the repo
# ...a usage limit hits mid-session...
codex-profile lease swap "$TOKEN" # next-best account, same home
codex exec resume --last - < followup.md # still warm, no repo re-readbegin— reserves the profile with the most remaining quota (same logic and exit codes asbest-auth), seeds a private throwawayCODEX_HOME, and records the reservation so a second run never grabs the same account. Prints the home path, or{profile, home, token, expires_at}with--json. The reservation lasts--ttlseconds (default 3600), a backstop in case the process dies without releasing it.swap <token>— the leased account hit a limit.swapwrites its refreshed credential back, marks it exhausted for an hour, and drops the next-best account into the same home. The session files undersessions/are left alone, so acodex exec resumestays warm.end <token>— writes the refreshed credential back to its profile and tears the lease down. It is idempotent and trap-safe: wire it to a shelltrapand a second call, or a call after the work already finished, is a clean no-op. Pass--profile <id>to assert the lease still belongs to the account you expect before writing.gc— deletes expired lease homes and reclaims any home a crashed run left behind.begincalls it opportunistically, so you rarely run it yourself.
Reservations live in the shared usage cache, and any account holding one is skipped by best-auth, exec, and other lease begin calls. Every write to that cache goes through a cross-process lock, so two agents reserving and releasing accounts at the same moment cannot drop each other's reservations or strand a refreshed credential.
--non-interactive and --timeout behave as they do for best-auth: skip Keychain prompts that would block a headless run, and guarantee the call exits. Selection exit codes match best-auth (2 no eligible profile, 3 no profiles configured, 4 usage unavailable, 6 keychain interaction required).
Codex Desktop still runs against its normal ~/.codex/ directory. This project
stores per-profile auth in macOS Keychain and swaps the selected profile into
~/.codex/auth.json when you switch.
When you switch profiles, the helper:
- Quits the running Codex instance
- Saves the outgoing live auth back to the matching stored profile
- Restores the selected profile's saved auth to
~/.codex/auth.json - Relaunches Codex normally
For usage data, the app fetches quota via codex app-server in a temporary
profile-scoped environment. The same path is used by the CLI's best-auth
command when it self-fetches usage before ranking.
The app stores auth tokens in macOS Keychain. On signed releases (DMG or Homebrew), macOS asks for Keychain approval once during initial setup. After that, profile switching should not prompt again.
Local unsigned builds may trigger a Keychain prompt each time a different binary reads or writes saved auth. Install the signed release to avoid repeated prompts.
If upgrading from an older unsigned build, run codex-profile keychain-repair once to rewrite saved auth with current access settings.
If a signed build still prompts for Keychain access, open Keychain Access.app, find the CodexProfileSwitcher entry, and add CodexProfileSwitcher.app under Access Control > "Always allow access by these applications."
Open Settings... > General for built-in support tools:
- Copy Debug Info copies app state plus recent redacted logs to the clipboard.
- Open Log opens
~/Library/Logs/CodexProfileSwitcher/CodexProfileSwitcher.log. - Report Bug opens the GitHub issue form.
Logs redact emails, bearer tokens, cookies, API keys, and OAuth fields before writing. From the terminal, codex-profile doctor prints a quick environment and saved-profile check.
- CHANGELOG.md — release history
- AGENTS.md — project structure, build commands, contribution guidelines
- SECURITY.md — vulnerability reporting
- docs/architecture.md — system design overview
- docs/DEVELOPMENT.md — development setup and workflow
- docs/repo-boundaries.md — source ownership and generated-path boundaries
- docs/RELEASING.md — maintainer release process
Auth and usage API patterns adapted from CodexBar by Peter Steinberger.
MIT
