Fast-switch between ChatGPT Business seats for the Codex CLI and desktop app.
macOS only at the moment. A ~5-second swap between accounts when one hits its 5-hour message limit, instead of the usual 2-minute manual sign-out / sign-in / 2FA / approve dance.
Pooling multiple ChatGPT Business seats for a single user is against OpenAI's Team plan terms of service. OpenAI's auth server actively enforces single-active-session per (workspace, device, client), which means this tool cannot give you concurrent sessions — only fast sequential ones. You are responsible for understanding your obligations under the plan you signed up for.
This project is published for educational and research purposes. Use at your own risk.
- One command to switch which ChatGPT Business seat your Codex CLI is signed in as, in ~5 seconds.
- Automatic 5-hour cooldown tracking per seat so you don't switch back to a seat that's still rate-limited.
- A launchd job that keeps the cooldown clock accurate in the background.
- A one-shot
app-nextcommand that quits Codex.app, swaps seats, and relaunches the app.
What this does not give you:
- Concurrent sessions across seats. OpenAI's OAuth server revokes prior tokens whenever a new one is issued for the same workspace + device. You can only have one seat alive at a time.
- Any way around the 5-hour limit itself. The tool just rotates which seat is bearing the limit.
Each seat is mapped to a dedicated Chrome profile that stays persistently signed into chatgpt.com with one of your accounts. Switching seats runs codex login, intercepts its OAuth URL, and opens the URL in the right Chrome profile via --profile-directory. Because chatgpt.com is already authenticated in that profile, the OAuth callback completes in seconds with zero or one click. Codex writes new tokens to ~/.codex/auth.json, which both the CLI and the desktop app read on launch.
State (current seat, cooldown timestamps, profile assignments) lives in ~/.codex-seats/state.json, guarded by flock.
- macOS (uses
launchd, AppleScript, andopen -na— Linux/Windows would need a port) - Codex CLI (
@openai/codex) - Python 3 (ships with macOS)
- Google Chrome (or Chromium-derived browser that honors
--profile-directory) - A ChatGPT Business workspace with multiple seats you control
git clone https://github.com/<your-user>/seatswitch.git ~/seatswitch
echo 'export PATH="$HOME/seatswitch/bin:$PATH"' >> ~/.zshenv
exec zsh
codex-seat initVerify the wrapper is on your PATH:
which codex-seat # should point inside the cloned repo
which codex # should point inside the cloned repo, BEFORE /opt/homebrew/bin/codexIf your Codex CLI lives somewhere other than /opt/homebrew/bin/codex, set:
export SEATSWITCH_REAL_CODEX=/path/to/codexOpen Chrome → profile menu (top right) → "Add". Create one profile per ChatGPT account you want to use. In each profile, navigate to chatgpt.com, sign in with that account, and check "keep me signed in".
List the profiles seatswitch can see:
codex-seat list-profilesMap each seat slot to a profile:
codex-seat assign seat1 "Default"
codex-seat assign seat2 "Profile 1"
codex-seat assign seat3 "Profile 2"
# ... up to seat6
codex-seat status # verifyClears expired 5-hour cooldowns once a minute so status is always accurate. Without it, expired cooldowns are still cleared on the next codex-seat next invocation, just lazily.
./scripts/install-launchd.sh # renders the plist with this repo's path and loads it
tail -f /tmp/seatswitch.log # watch the ticksUninstall with ./scripts/uninstall-launchd.sh.
codex # run Codex CLI against the current seat
codex-seat next # cool current seat, fast-switch to next, relaunch codex
codex-seat switch seat3 # jump to a specific seat
codex-seat status # current seat, cooldowns, emails per seat
codex-seat app # launch Codex.app on the current seat
codex-seat app-next # quit app, fast-switch, relaunch app# In Codex CLI: exit (Ctrl-D), then:
codex-seat next # ~5s
# In Codex.app: just one command, no manual quit:
codex-seat app-next # ~8s including app relaunch| command | what it does |
|---|---|
codex [args...] |
run Codex CLI against the live auth (shim around codex-seat run) |
codex-seat init |
create state dirs and empty state.json |
codex-seat list-profiles |
list Chrome profile directory names |
codex-seat assign seatN PROFILE |
map a seat to a Chrome profile directory name |
codex-seat status |
current seat, profiles, cooldowns, decoded emails |
codex-seat switch seatN |
fast-switch to a specific seat; relaunches Codex.app if running |
codex-seat next [args...] |
cool current, fast-switch to next eligible, launch codex |
codex-seat exhausted [seat] |
mark a seat exhausted manually (default: current, 5h cooldown) |
codex-seat clean-cooldowns |
clear expired cooldowns (launchd calls this every minute) |
codex-seat app |
launch Codex.app against the current seat |
codex-seat app-quit |
quit Codex.app via AppleScript |
codex-seat app-next |
quit app, fast-switch to next eligible, relaunch app |
codex-seat login seatN |
manual interactive codex login + snapshot (rarely needed) |
codex-seat run [args...] |
what the codex shim invokes; exec's real codex |
| variable | default | meaning |
|---|---|---|
SEATSWITCH_REAL_CODEX |
/opt/homebrew/bin/codex |
path to the real codex binary |
SEATSWITCH_BROWSER |
Google Chrome |
application name passed to open -a |
~/.codex-seats/
├── state.json # currentSeat, lastAuthMtime, per-seat cooldownUntil/lastUsed/browserProfile
├── .lock # flock target
├── .last-login.log # captured stdout/stderr of the most recent `codex login` (for debugging)
└── seat1/.../seat6/
└── auth.json # snapshot of that seat's tokens after most recent login (informational only)
- Chrome profile signed out → OAuth doesn't auto-complete and
codex logintimes out at 90s. Fix: open the affected Chrome profile, re-sign-in to chatgpt.com, retry. - Desktop app and CLI share
~/.codex/auth.json. They can't be on different seats at the same time. The desktop app caches auth in memory at launch, so swapping seats while the app is running has no effect until the app is quit and relaunched —codex-seat app-nexthandles this for you. auth.jsonsnapshots in~/.codex-seats/seatN/are not used to restore sessions. OpenAI revokes them as soon as a different seat logs in. They're kept around for thestatuscommand's email column.codex login --device-authis not used. We rely on the standard OAuth callback flow so that chatgpt.com cookies in the Chrome profile carry the authentication.- Two simultaneous
codexprocesses could race over~/.codex/auth.jsontoken refreshes. Theflockonly protects state.json, not the live auth file written by codex itself. Either run one at a time, or accept the small chance of needing to swap again.
If you want to understand why this design is what it is — see the long version above. Short version: OpenAI's OAuth server enforces single-active-session per (workspace, device, client). We confirmed empirically that logging in seat N revokes seat N-1's refresh token. The only way to get fast seat switching on a single machine is to make each fresh login fast — which is what the pre-signed Chrome profile trick accomplishes.
Issues and PRs welcome. The whole thing is one Python file plus a bash shim, so it's easy to dive into.
The bits that hard-code macOS, with where to look:
open_url_in_chrome_profile()inbin/codex-seat— usesopen -na "Google Chrome" --args. Linux:google-chrome --profile-directory=.... Windows:chrome.exe --profile-directory=....list_chrome_profiles()inbin/codex-seat— uses~/Library/Application Support/Google/Chrome/. Linux:~/.config/google-chrome/. Windows:%LOCALAPPDATA%\Google\Chrome\User Data\.is_codex_app_running()/quit_codex_app()/launch_codex_app()— usesosascriptandopen -a. Linux:pgrep+pkill+ the desktop app binary. Windows: PowerShellGet-Process/Stop-Process/Start-Process.CODEX_APP_PATHand the defaultSEATSWITCH_REAL_CODEX— platform-dependent paths.launchd/andscripts/install-launchd.sh— replace with a systemd user unit on Linux or a Task Scheduler XML on Windows.
The CLI swap half is fairly tight — the porting work is mostly the desktop-app half and the background scheduler. Firefox container support would replace the Chrome profile mechanism with ext+container: URLs.