Switch Claude Code and Codex accounts, and see what each one has left before you commit to it.
If you run more than one Claude subscription you know the shape of this problem.
You are deep in something, the weekly limit lands, and the only road to your
other account is /login — which signs you out everywhere, drops whatever
sessions you had going, and tells you nothing about whether the account you are
moving to has any room left either. An hour later you do the whole thing again in
reverse.
ccs keeps every account logged in at once, shows you where each one stands, and
moves between them in place. Claude Code sessions that are already running follow
along on their next request. Nothing restarts, and nothing gets signed out.
Seeing before switching. Claude Code and Codex have separate sections with columns suited to their usage limits. Claude shows its session, weekly and model-specific windows. Codex shows its shared quota and every additional pool the API reports, with all available windows and reset countdowns. You can see which account has room before switching.
Never logging in again. Each account is stashed with its own credentials. Switching installs one of them over the live file; it never signs the other one out. Going back is another switch, not another login.
Live sessions. A Claude Code switch reaches sessions already running, mid-task, without a restart. That is the difference between "I'll switch accounts" being a two-second decision and being a five-minute interruption.
Notices into a session. A Claude Code session that runs ccs notify from
inside itself gets told things in its own conversation: switch (which account
it is now on, so the model knows its prompt cache just went cold), and — while
ccs watch is running — session-high (the account in use has crossed 90% of
its five-hour window), session-reset (an account's window came back, and
whether its weekly resets sooner than the active one's) and weekly-reset.
Name the kinds you want, or none for all of them; ccs notify off stops them.
ccs watch --every 300 --high 90 are the defaults. A session that bypasses
permission prompts holds a notice for review unless the sender attests the
same mode: subscribe from such a session with ccs notify --bypass.
Rotation. ccs watch --rotate agent,work,robin also switches for you: when
the account in use is one of those and its session has run high (or its weekly
is spent), the pooled account whose weekly window resets soonest and still has
room takes over — quota about to be forfeited is burned first. An account in
use that is not in the pool was chosen by hand and is never touched. Running
sessions follow the switch like any other, and hear a switch notice if they
asked.
A gateway for other tools. ccs serve puts the Anthropic Messages API on
127.0.0.1:4141, answering as whichever account is in use. Anything that speaks
the API — pi through its models.json, say — gets every
account you have stashed and never logs in itself; ccs use, the picker and
ccs watch --rotate move it along with your sessions. With a --rotate pool of
its own, a request the account in use is too limited to answer is quietly sent
again as the next pooled account. See below.
Codex accounts too. Choose Codex in ccs add to stash a ChatGPT login for Codex
CLI the same way, and the table, picker, watcher, menu bar app and gateway
take it from there: what each Codex account has left, a click to switch
~/.codex/auth.json, rotation among Codex accounts, and pi or Aside's
openai-codex provider served through the gateway. Codex's login and Claude
Code's are two slots, each with its own account in use.
Pinning. One session on one account, every other session left where it is. This is the feature that changes how you work — see below.
You need a Rust toolchain. Then:
git clone https://github.com/orthory/ccs
cd ccs
make installThat builds a release binary and puts ccs on your PATH under $CARGO_HOME/bin.
Somewhere else:
sudo make install PREFIX=/usr/localmake help lists the rest — make check runs formatting, clippy and the tests,
make uninstall takes it back off.
Start with the one you are already signed in as, then log in to the others:
ccs add --current # choose Claude Code or Codex, then stash its current login
ccs add # choose a provider, then log in to another account
ccs add # ...and anotherccs add first opens a provider menu. Arrows or j/k select Claude Code or
Codex; Enter continues and Esc or q cancels. It then runs claude auth login
or codex login against a throwaway config directory, keeps the credentials it
mints, and destroys the directory. The account you are currently
using is never touched — no logout, no re-login, and sessions running against it
carry on straight through the login you are doing in the next window.
Steer Claude's login page with --email <address>, --console (Console billing rather
than a subscription), or --sso. Stash under a name of your choosing with
--name <slug>; otherwise the slug comes from the email.
After that you are done logging in. ccs use moves between stashed accounts
directly.
ccs add --current # choose Codex to stash its current login
ccs add # choose Codex to log in to another accountCodex accounts have their own section in the picker and ccs ls.
ccs use installs the selected account into ~/.codex/auth.json (or $CODEX_HOME's).
Use Codex's /status to check an existing session after switching; if it still
shows the previous login, resume the session. Claude Code's slot is untouched,
and the other way round: ccs ls marks one account in use per provider, and
ccs status offers Claude Code, Codex, or both. Commands naming an account,
such as use, pin and rm, take their provider from that account.
This integration reads Codex's file credentials. If your Codex uses an OS
credential store, select cli_auth_credentials_store = "file" in its config
and log in there before choosing Codex in ccs add --current. See
Codex authentication.
ccs pin selects file credentials for its Codex launch.
Codex shows the reported shared quota windows and a model availability column
when the backend supplies one. Astra appears alongside those windows, in the
same place that model-specific limits occupy in the Claude section. Its cell
says available, unavailable, or back 1h30m when a future availability time
is reported. It adds credits unlock when the response says credits would
provide access. Missing availability is unknown; an elapsed countdown never
overrides the backend's availability flag.
Astra's availability is distinct from a percentage-based quota. No separate
GPT-5.6 or Astra percentage is invented. Spark is hidden from terminal tables,
text status and switch warnings. JSON and the usage cache retain its readings,
along with model_usage metadata for every reported model. Other named quota
pools appear on continuation rows with a POOL column when needed. Every
reported window is retained, including both five-hour and weekly windows when
present. The 5H column is always visible, showing — when the response does
not include that window; a dash does not imply zero usage or unlimited access.
The backend describes quota windows
in its usage response schema.
Old cached Codex readings that lost a named pool's window duration appear under
CACHED WINDOW until the next poll replaces them. Refresh with r in the picker
or run ccs watch to obtain the complete readings.
Codex accounts can also be pinned and can receive usage notices:
ccs pin codex-work # launch Codex on this stashed account
ccs pin codex-work -- resume --last # resume within that pin's historyThe account determines which client ccs pin launches. A Codex pin sets
CODEX_HOME to ccs/pens/<account>/, keeps auth.json private, and links
config.toml, AGENTS.md, skills, plugins, rules, prompts and named
*.config.toml profiles to the original Codex home. These are Codex's own
settings and skills; Claude configuration is not translated. Conversation
history, databases, locks and other runtime files use the pin by default; explicit
paths in your Codex configuration still apply. A later
launch with the same account reuses that home. ccs rm removes matching
pinned credentials while preserving Codex conversations.
Inside a Codex session, have the agent run:
ccs notify # detect this Codex thread and subscribe
ccs notify session-high # or only the high-usage notice
ccs notify off # unsubscribe this thread
ccs status --codex --cached # last account usage reading, no API call
ccs status --codex --cached --json # includes polled_at for scripts
ccs status --codex # refresh only Codex's readingRun ccs watch in another terminal to poll and generate usage notices. The
watcher sends session-high for the account active in its credential home,
and session-reset/weekly-reset for that provider's accounts. Notices and
rotation use shared windows; additional named pools are shown in usage views. Run the
watcher with a pin's CODEX_HOME to track that pin's active account. Switch
notices go only to sessions registered against the home that changed.
Codex notices use codex queue --thread … --message …, with the thread ID
Codex exports as CODEX_THREAD_ID. They are queued conversation messages,
not a native status-line widget. Registration verifies that the installed
CLI supports this command (verified with 0.153.4); older builds get a clear
error and can still use cached status. Queue failures are reported and keep
the subscription for future events. --bypass is only for Claude's inbox;
Codex keeps its session permissions.
In a terminal, ccs notify and ccs serve --key offer a provider menu too.
Without a terminal, notices detect the calling client from its session
environment; if both clients' variables are inherited, select --codex or
--claude explicitly. Cached status uses the credentials in that home, so it
reports the pinned account even when the global active pointer differs.
These readings track subscription windows, not individual-turn token counts.
For scripts, --claude and --codex bypass provider menus. Adding an account
without a terminal requires one of these selectors. JSON or piped status shows
both providers by default; piped ccs serve --key retains its Claude default,
with ccs serve --key codex selecting Codex. ls, watch and serve operate
across providers without a provider menu.
Run ccs with no arguments and you get the picker. Accounts are grouped by
provider, then email. The numbers shown by ccs ls use the same order for
use, pin and rm; prefer stable slugs in scripts.
For example, the Claude section:
Claude Code
ACCOUNT PLAN SESSION WEEKLY FABLE
> 1 you@example.com max20x ▍░░░ 10% 3h54m ██▏░ 55% 6h24m ████ 100% 6h24m <- active
2 you+alt@example.com max5x ░░░░ 0% ▋░░░ 18% 2d11h ▉░░░ 22% 2d11h
3 team@example.org pro ██▍░ 61% 1h44m ███▌ 88% 5d19h ██▊░ 70% 5d19h
up/down select enter switch esc unselect r refresh q quit updated just now
The Codex section shows its quota windows and model availability:
Codex
ACCOUNT PLAN 5H WEEKLY ASTRA
4 you@example.com codex pro — █▏░░ 29% 2d11h available <- active
5 you+alt@example.com codex pro — ██▌░ 65% 2d11h back 1h30m
Additional pool names and model IDs come from the API. Pool rows belong to the account above them; only accounts are selectable. Switching to an account with an unavailable model prompts for confirmation, just as a spent quota does. Watcher notices and rotation continue to follow shared quota windows.
Arrows or j/k move and home/end jump to the ends. The table carries the
readings; the footer carries keys and confirmations. ccs status shows detailed
readings separately. enter asks before it does anything and names restrictions:
you@example.com on Claude Code: no Fable left. Switch anyway? [y/n]
Answering yes switches, and the picker stays where it is — the active marker moves to the row you picked and the footer says what happened:
Claude Code
ACCOUNT PLAN SESSION WEEKLY FABLE
1 you@example.com max20x ▍░░░ 10% 3h54m ██▏░ 55% 6h24m ████ 100% 6h24m
> 2 you+alt@example.com max5x ░░░░ 0% ▋░░░ 18% 2d11h ▉░░░ 22% 2d11h <- active
3 team@example.org pro ██▍░ 61% 1h44m ███▌ 88% 5d19h ██▊░ 70% 5d19h
switched to you+alt@example.com
Nothing is re-fetched to draw that: a switch spends nobody's limits, so the readings on screen are as true after it as they were before. Switch again from the same table if the first one was wrong — the terminal you leave the picker in still gets the record of where you ended up.
esc backs out one step at a time. From a question it takes you back to the list;
from the list it puts the selection away entirely, which leaves enter with
nothing to act on:
Claude Code
ACCOUNT PLAN SESSION WEEKLY FABLE
1 you@example.com max20x ▍░░░ 10% 3h54m ██▏░ 55% 6h24m ████ 100% 6h24m <- active
2 you+alt@example.com max5x ░░░░ 0% ▋░░░ 18% 2d11h ▉░░░ 22% 2d11h
3 team@example.org pro ██▍░ 61% 1h44m ███▌ 88% 5d19h ██▊░ 70% 5d19h
up/down select r refresh q quit updated just now
Move again and the selection comes back. One more esc from there leaves the
picker; q and ctrl-c leave from anywhere.
If you already know where you are going, skip the picker:
ccs use work # slug, email, unambiguous prefix, or the index from `ccs ls`
ccs use 2
ccs ls # the same table, printed and gone
ccs status # just the account in use, in detailccs use switches straight away and is the one that leaves you at your prompt;
the picker is where you switch and then keep looking. It only stops to ask when
the account you are switching to has a limit already at 100%, and --force skips
even that.
A switch is global — every session follows it. Sometimes that is exactly wrong:
you want this terminal on the work account and everything else left alone. That
is ccs pin:
ccs pin # pick from the table, then launch
ccs pin work # skip the picker
ccs pin work -- --continue # anything after `--` is handed to Claude CodeIt picks an account the same way ccs does, then starts Claude Code on it —
which is the one thing the picker cannot stay up for, because the session takes
the terminal. That
session is the only thing that moves. Every other session stays on the account in
use, and a later ccs use leaves the pinned one exactly where it is. Nothing
displays the account on its own. ccs status inside the session names it, and
CLAUDE_CONFIG_DIR points at the pen — which is named for the account — so a
status line can keep the answer in front of you without asking the network.
Run several at once, one terminal each, and you are working three accounts in parallel with three separate limit budgets.
A pinned session is a normal session in every way but one. It gets a pen — a
configuration directory of symbolic links back to your real one, whose only file
of its own is the credentials — and CLAUDE_CONFIG_DIR points at it.
Shared, live, with every other session: settings, skills, plugins, agents,
slash commands, MCP servers, project trust, conversation history, todos — and the
global .claude.json alongside them. These are links, not copies, so a pen never
drifts from what it mirrors, and a skill you add inside a pinned session is a skill
every session has. There is no syncing step because there is nothing to sync.
The pen's own, shared with nothing: the credentials. That is the entire point
of the pen, and it is the only real file in it. A ccs use elsewhere rewrites the
live credentials and leaves the pen's standing.
Also not mirrored: the account stash itself and the credential write lock. The
stash stays reachable from inside a pen anyway — a pen records the configuration it
was cut from, so ccs inside a pinned session reads your real accounts, and
ccs use in there re-pins that one session rather than moving everybody. ccs rm
takes an account's pen away with it.
If Claude Code ever replaces one of those links with a real file of its own, the pen keeps that file from then on rather than clobbering it back to a link.
ccs serve # or: ccs serve --port 4141 --rotate agent,workIt prints the fragment to paste into pi's ~/.pi/agent/models.json:
{
"providers": {
"anthropic": {
"baseUrl": "http://127.0.0.1:4141",
"apiKey": "!ccs serve --key"
}
}
}That is the whole of it. Overriding only baseUrl on the built-in provider keeps
every Claude model pi already knows about, and the key is fetched by running the
command, so nothing secret sits in the file. A client launched from the desktop
rather than a shell may not have ~/.cargo/bin on its PATH; spell the command
out as !/Users/you/.cargo/bin/ccs serve --key if the model shows as
unavailable. Aside reads the same file at
~/.aside/u/0/models.json, and needs exactly that. Each request goes out as the account
in use, with its access token refreshed on the way when it has expired, and the
answer is streamed back as it arrives.
The key is sk-ant-oat-ccs- and 32 random hex digits, minted once into
~/.claude/ccs/gateway.key. The prefix is what pi keys on to treat a key as an
OAuth token — so pi itself sends the bearer header, the OAuth betas, and the
Claude Code identity line at the head of the system prompt, and the gateway has
no reason to read a body. The suffix is what stops another process on the same
machine from spending your subscription: a request without it gets a 401 and
never leaves the machine.
A request the API turns away with a 429 is, when --rotate names a pool, sent
again as the first pooled account not yet found limited, before a byte has
reached the client. Without a pool the 429 is relayed as it is. A 401 on a
token this tool thought was fresh is taken for a session having refreshed the
live credentials underneath it; the copies are brought level and the request
sent once more.
Only 127.0.0.1 is listened on, and only paths under /v1/ (Anthropic) and
/backend-api/ (Codex) are relayed.
The Codex side works the same way with a second key. pi's built-in
openai-codex provider is told "baseUrl": "http://127.0.0.1:4141/backend-api"
and "apiKey": "!ccs serve --key codex"; the startup snippet prints both
providers. That key is shaped as a token, because pi reads the ChatGPT account
id out of the key it is given and sends it as a header: the gateway checks the
whole string, then puts the real account's token and id on the request. pi
opens a socket first for Codex; the gateway refuses the upgrade and pi falls
back to server-sent events for that session.
make install-app # macOS: /Applications/ccs.app; Linux: ~/.local/bin/ccs-app
open /Applications/ccs.appOne window on every platform: every account with its bars and reset times,
click one to switch, and a spent account asks first. Below them the two
switches. Gateway serves the API on the port you set. Rotate
automatically hands the watcher a pool of the accounts you tick, and
Notifications turns its session-high, session-reset, weekly-reset
and rotate lines into system notifications. Launch at login keeps a
LaunchAgent on macOS and an autostart entry on Linux, and takes effect from
the next login. All of it is remembered
under ccs/app.json.
On macOS there is a menu bar item too: the active Claude account's session on the bar, and the window on a click.
The app links ccs as a library, so there is one process: the watcher runs on
a thread of its own and is the one thing that polls, the gateway's listener
and desk run on threads of the same process, and the window reads what the
watcher last wrote down. Quitting takes the gateway down with it. It is
written in Ice, a checked UI
language for iced, taken as a git dependency at a pinned revision since it is
not on crates.io; make app builds it, make test-app runs its tests, and
make lint-app runs the Ice checker (cargo install --git https://github.com/byeongsu-hong/ducktape-ui cargo-ice).
Switching a live session. Claude Code checks the mtime of its credentials file
each time it resolves credentials, and drops its in-memory auth when the file has
moved underneath it. ccs writes the replacement to a sibling temp file and
renames it into place, so a reader sees either the old file or the new one and
never a half-written one — and the rename freshens the mtime that running sessions
are watching. That is the whole trick. There is no daemon and no IPC.
On macOS, where the credentials are not a file. Claude Code keeps them in the
login keychain there, reads that in preference to the file, and writes the file
only when the keychain turns it away — so a switch written to the file would be a
switch nothing reads. ccs writes the item instead, under the name Claude Code
gives it: Claude Code-credentials, keyed to your login name. A session with no
credentials file to watch compares the token in the keychain instead, so a switch
reaches running sessions the same way it does anywhere else.
The item is namespaced by configuration directory — CLAUDE_CONFIG_DIR decides
which — which is what gives every pen credentials of its own, and what keeps a
pinned session pinned. ccs rm takes an account's item away with its pen, and an
interrupted ccs add leaves none behind.
Not fighting over the file. Claude Code takes a lock beside the credentials
file when it refreshes tokens. ccs takes the same lock, so a switch can't
interleave with a refresh and lose one of the two writes.
Keeping stashed tokens alive. This is the part that is easy to get wrong.
Claude Code refreshes access tokens in place, so a stashed copy goes stale the
moment its account is used, and a refresh can rotate the refresh token — which
makes the copy you were holding worthless. So ccs folds the live tokens back into
the stash on the way out of an account, refreshes any stashed token that has
expired before polling it, and writes down whatever comes back before doing
anything else with it. When the account it refreshed is the live one, the
credentials file gets the new token too, so running sessions are never left holding
one that has been superseded.
Reading usage. The limits come from the same OAuth endpoint Claude Code uses
for /status, one request per stashed account. The columns are built from whatever
the API reports rather than from a fixed list, so a newly scoped model turns up as
its own column without a change here.
Leaving the reading behind. Every poll is written down on the way past, one
file per account under ccs/usage/, stamped with when it was taken. Nothing in
ccs reads them back — they are there for anything that has to show where an
account stands far more often than a poll can be afforded, a status line above a
prompt being the case they exist for. The limits are stored exactly as the
endpoint reported them, so a reader draws whatever windows it finds rather than
knowing a list of them. A failed poll leaves the last reading standing instead of
blanking it, and the stamp is what says whether it is still worth believing;
ccs status is the cheapest way to freshen one, costing the account in use a
single round trip. A reading looks like this, and nothing but the endpoint decides
how many limits are in it:
{
"polled_at": "2026-08-28T02:31:04Z",
"limits": [
{ "kind": "weekly_all", "percent": 55.0, "severity": "normal",
"resets_at": "2026-09-01T14:00:00Z", "scope": null },
{ "kind": "weekly_scoped", "percent": 100.0, "severity": "critical",
"resets_at": "2026-09-01T14:00:00Z",
"scope": { "model": { "display_name": "Fable" } } }
]
}Polling. The picker polls with the screen already up — on open, on r, and
every ten minutes on its own — so the list is never taken away to fetch. An
unattended poll waits for a lull rather than freezing the list under you, the
footer says how old the reading is, and a poll that fails says so there and leaves
the last good reading standing. The interval is long because each poll costs a
request per account against an endpoint that rate-limits. The countdowns don't wait
on it: they are recomputed from the reset instants every time the screen is
painted, so only the percentages are as old as the footer says.
| command | what it does |
|---|---|
ccs |
the picker |
ccs ls |
every stashed account and what it has left |
ccs use <account> |
switch that provider's login |
ccs pin [<account>] |
start a session confined to one account |
ccs add |
choose a provider, then log in and stash another account |
ccs add --current |
choose a provider and stash its current login |
ccs rm <account> |
forget a stashed account |
ccs status |
choose a provider or both; show usage and reset times |
ccs status --codex --cached |
this Codex home's last reading, without polling |
ccs notify [<kind>...] |
have notices delivered into the calling session |
ccs watch |
poll every account and raise notices; --rotate switches too |
ccs serve |
serve the API on loopback as the account in use |
ccs serve --key |
choose whose gateway key to print |
<account> is a slug, an email, an unambiguous prefix of either, or the index from
ccs ls. ccs pin without one opens the picker; ccs use without one is an
error rather than a guess.
-f/--force switches even into an account with nothing left. --json on ls
and status gives you the same data for scripts; status keeps the shape it
always had, one object, with the Codex account under codex when both are
logged in. status --codex and status --claude read only that provider.
status --cached reads the account's last reading without polling or refreshing
tokens, includes polled_at, and reports a missing reading rather than zero usage.
For all stashed accounts, a status line wants
ccs ls --cached --json instead — the last readings ccs watch wrote down,
with a polled_at on each, and no poll — because it repaints far more often
than a poll can be afforded.
Columns are built from whatever the usage endpoint reports rather than from a list in the code, so this describes what it returns today and is not a schema. Today that is three:
- session — the rolling five-hour window
- weekly — the all-models weekly window
- Fable — the weekly window scoped to that model
A model-scoped limit is drawn under the model's own name, so if the endpoint starts scoping another one it gets a column without a change here.
Green is fine, yellow is worth knowing about, red is spent. An account with any
limit at 100% is dimmed in the table, and ccs asks twice before walking into it.
A — means that account reported no limit of that kind at all, where another
account did.
~/.claude/.credentials.json the live account, as Claude Code reads it
— on macOS, the login keychain instead
~/.claude/ccs/accounts/*.json one stashed account each, mode 0600
~/.claude/ccs/pens/<account>/ one pinned session's configuration each
~/.claude/ccs/usage/*.json what each account last had left, and when
~/.claude/ccs/state.json which slug is currently installed
~/.claude/ccs/notify.json Claude session subscriptions
~/.claude/ccs/notify-codex.json Codex threads, homes and notice subscriptions
~/.claude/ccs/gateway.key what a client presents to `ccs serve`, mode 0600
~/.claude/ccs/codex.key the same for the Codex route
~/.codex/auth.json the Codex account in use, as Codex CLI reads it
~/.claude/ccs/.login-<pid>/ a login in progress, destroyed when it ends
Those account files hold OAuth refresh tokens. They are written 0600 inside a
0700 directory, and each one is worth exactly as much as a password. Don't sync
them anywhere you wouldn't sync a password. That is as true on macOS, where the
stash is these same files: the keychain holds the account in use and a pinned
session's, and the stash behind them is on disk either way.
A keychain item ccs creates for a pen is opened to the applications you run, the
way the plain file already is to the processes you run — otherwise a pinned session
would start by asking you to unlock something. The item Claude Code made for the
account in use is updated in place and keeps the access it came with.
A login directory left behind by an interrupted run is swept on the next ccs add.
Each is named after the process that owns it, so a run still in flight is never
swept out from under itself.
CLAUDE_CONFIG_DIR is honoured the same way Claude Code honours it, which is what
lets ccs work correctly from inside a pinned session, and on macOS what names the
keychain item a session reads. CLAUDE_SECURESTORAGE_CONFIG_DIR is honoured there
too, for the same reason. CCS_CLAUDE_BINARY points at a claude that isn't on
PATH, CCS_CODEX_BINARY at a codex, and CODEX_HOME is honoured the way
Codex honours it. NO_COLOR does what you expect.
ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN take
precedence over the stored credentials for any session that inherits them — so a
session with one of those set ignores whatever account you switched to. ccs says
so rather than letting you wonder.
MIT. See LICENSE.