tokenmaxxing
v1.80.0
Published
Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.
Readme
tokenmaxxing
Automatic Claude Code account switching. Run claude exactly as you always do; each session starts on the pooled account with the most headroom per running session, and when that account nears its usage limit, tokenmaxxing resumes the session on a fresher account at a safe turn boundary - same conversation, compacted on the account it leaves and restarted under another account's credential store with a first prompt that asks Claude to continue. Works across many concurrent sessions, which spread over the pool instead of draining one account together. When the whole pool is at its limit, a session queues first come first served for a reset (at most four sessions wait on one account) and pauses with a countdown until its assigned reset, when that reset falls within policy.maxWaitMs (default 1h - a longer wait stays put rather than parking your terminal for hours).
Scope: Claude Code on macOS and Linux, plus OpenAI's Codex CLI as a second switching pool; grok and opencode-go as status-only pools. It pools subscription accounts (Pro/Max), not API keys; the opencode-go pool, which holds Zen API keys, is the one exception.
$ claude
...you work normally; the statusline's ◆ marks this session's account...
↻ tokenmaxxing: compacting the conversation on [email protected] before the move...
↻ tokenmaxxing: moving to [email protected] - resuming...
⏳ tokenmaxxing: all accounts at their limit. Resuming on [email protected] when it resets (Ctrl-C to resume now).Why
Every pooled account owns one Claude Code credential store under ~/.config/tokenmaxxing/stores/. A thin claude supervisor on your PATH picks an account for each session and points CLAUDE_SECURESTORAGE_CONFIG_DIR at its store, so sessions on different accounts never share a credential, while sessions on the same account share its store and Claude Code's own refresh keeps them in step. A session moves only by respawn: the supervisor stops it at a committed turn boundary (the transcript is already on disk, nothing is lost) and runs claude --resume <id> under another store. tokenmaxxing writes a store once at onboarding and never again; it never copies a credential between stores, because a refresh rotation revokes the previous access token and two copies of one grant break each other. Everything else about claude is unchanged - all flags, MCP, hooks, and skills pass through. Print mode and the non-interactive subcommands run without a seat, on whatever login the environment names.
Install
Requires Bun and Claude Code, on macOS or Linux.
bun add -g tokenmaxxing
tokenmaxxing initOr with Nix (same source-run-by-Bun package; init still owns the stores, the claude shim, and settings merges). Install onto PATH first, then init: nix run ... -- init alone leaves supervisor shims without a stable tokenmaxxing on PATH after the ephemeral run exits.
nix profile install github:anaclumos/tokenmaxxing
tokenmaxxing initnix-darwin:
inputs.tokenmaxxing.url = "github:anaclumos/tokenmaxxing";
modules = [
inputs.tokenmaxxing.darwinModules.withOverlay
{ programs.tokenmaxxing.enable = true; }
];Home Manager:
imports = [ inputs.tokenmaxxing.homeManagerModules.default ];
programs.tokenmaxxing.enable = true;
programs.tokenmaxxing.package = inputs.tokenmaxxing.packages.${pkgs.system}.default;With either module, run tokenmaxxing init afterwards.
init verifies and pins the real claude binary, opens an isolated claude session for your first pooled account (run /login there, even if you are already signed in; the login you already have stays for sessions started outside the supervisor), installs the claude supervisor and the xx entry point, five settings.json entries (the tokenmaxxing statusLine, a subagentStatusLine, a Stop hook, a StopFailure hook, a SessionStart hook), the periodic check timer, and the usage hub service, and adds the supervisor's bin dir to PATH in your shell rc (idempotent; it must sit ahead of the real claude to intercept it). Restart your shell, then add more accounts and go:
tokenmaxxing add # logs one in, in isolation, into its own store
claude # use claude as always; each session gets its own accountCommands
| command | what it does |
|---|---|
| tokenmaxxing init | verify and pin the real claude, log the first account in (isolated) + install supervisor, hooks, PATH line, check timer, and hub service |
| tokenmaxxing init --codex | same for codex: log in the first account (isolated), install codex supervisor + Stop hook |
| tokenmaxxing init --grok / init --opencode-go | pool the first grok login or opencode-go key, status-only: no supervisor, no switching |
| tokenmaxxing add [--codex \| --grok \| --opencode-go] | register an additional account in that pool (isolated login, harvested once into its own store) |
| tokenmaxxing auth [--codex \| --grok \| --opencode-go] [sel \| --all] | reauthenticate a pooled account in place: bare lists the pool (emails shown) and asks which; a selector targets one account and tells you the email to sign in with; --all walks every account that is flagged or has no usable credential in its store, one by one |
| tokenmaxxing status [--cached] | every pool: accounts with 5h / weekly / per-model usage bars, live session counts, exhausted-until-reset; --cached renders the stored figures without sampling |
| tokenmaxxing config | the config path and the effective values; edit the file in an editor, a bad value fails the next load with the field name |
| tokenmaxxing check | fold fresh tees, sample up to three stale accounts, and self-update a Bun global install once a day; the periodic timer runs this every tick |
| tokenmaxxing doctor | verify the install: PATH order, claudeBin, the five settings entries, the timer, the hub service, the credential identity of every Codex store and of every Claude store whose access token is still fresh (an expiring Claude token is reported as unverifiable, not as a failure), codex hook trust, and shell aliases that shadow claude |
| tokenmaxxing rename [--codex \| --grok \| --opencode-go] <sel> <label> / rm [...] <sel> | manage a pool (one email can hold both a claude and a codex account); rm is refused for an account with a running supervised session |
| tokenmaxxing serve | serve the CLIProxyAPI-compatible usage API on http://localhost:8317 (hub.port) so a dashboard such as T3 Code's "Add a CLIProxyAPI hub" shows the quota of every pooled Claude and Codex account; the management key is the contents of hub-key in the state directory, minted on the first run, and init runs it as a background service beside the check timer. Details in Usage hub |
| tokenmaxxing uninstall [--yes] | print the targets, then remove the shims, settings entries, codex Stop hook, check timer, hub service, and rc PATH line (accounts/stores kept); refused without --yes when HOME is the login home |
| --json | machine-readable output: one JSON document on stdout for status, config, and check (ok mirrors the exit code, failures add error) |
How switching decides
Each session runs on its own seat. One rule: while the seat is under its bars, nothing happens; once it is at or over a bar, the session moves to the usable account that ranks first. The session bar is thresholds.session (90) minus policy.projectionMargin (3), so 87% of the 5-hour window by default, and the weekly bar is 98%. The bars also screen candidates on any of:
- Session (5-hour) or week (all models) - the aggregate windows, fed free/push-based by the statusLine into one tee per account.
- Per-model weekly cap - the most capable model (Fable) has its own tighter weekly limit that binds before the aggregate (per-model caps currently exist only for Sonnet and Fable, and Sonnet's is generous). tokenmaxxing reads it from the direct no-spend usage read for the seat (free, 0 tokens, one attempt per interval) whenever the seat's tee is older than
policy.usagePollTtlMsor the session's model is one ofpolicy.switchModels, backing off after a failed sample, so a Fable session moves on the Fable cap while a Sonnet session rides the aggregate. A session launched with--modelon another family (claude --model opus) ignores the Fable cap from launch on: it is placed on, and moves to, an account whose Fable cap is spent as long as that account's session and weekly windows have headroom. The exception is a move off an account the server already refused (an enforced-limit wall), which screens every configured family.
The bars' headroom is deliberate: it's the budget to reach a clean turn boundary before the account's real limit. The session bar sits lower (90, minus the margin) because a 5-hour reset is cheap to sit out; weekly quota is use-it-or-lose-it, so it drains closer to the limit (98).
Launches and moves rank usable accounts by session-window headroom per running session, (bar - used) / (sessions + 1), so two fresh accounts alternate and a third session goes to whichever has more headroom left per session; ties break by pace pressure (remaining percent of the binding gated per-model cap, or of the weekly aggregate for an account with no gated row, divided by time to its reset, highest first), then soonest weekly reset, then lowest weekly use. Organization membership is not an input.
- Every periodic check tick folds fresh statusline tees into the index and samples up to three accounts whose last sample attempt is oldest, skipping any attempted within
policy.usagePollTtlMs, with the direct no-spend usage read; a move reads the cached figures, which are as fresh as each account's last successful attempt or its sessions' latest push. An expired stored access token stays unmeasured until a session on that store refreshes it. - When no account is usable, the session queues first come first served onto a reset target (at most four waiters per account) and pauses until that reset if it lands within
policy.maxWaitMs, and otherwise stays put.
See How switching decides for the policy and the cache-cost profile for measurements and their limits.
Configuration
~/.config/tokenmaxxing/config.json (every field optional):
{
"thresholds": { "session": 90, "weekly": 98 },
"policy": {
"projectionMargin": 3,
"switchModels": ["fable"],
"usagePollTtlMs": 90000,
"maxWaitMs": 3600000,
"checkIntervalMs": 60000
}
}thresholds.session and thresholds.weekly are the two bars, one number each (an array thresholds.session, the former ladder, fails config loading with a message naming the field); projectionMargin is a fixed safety margin subtracted from the session bar only (default 3), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; switchModels names the models whose per-model cap triggers a move; usagePollTtlMs is how long a usage sample stays fresh, for a seat's own sample and for the accounts each check tick samples; maxWaitMs bounds the depleted-pool countdown - a reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); checkIntervalMs is the periodic check tick (default 60s), which init writes into the timer - re-run tokenmaxxing init after changing it so the timer unit picks up the new tick. claudeBin, codexBin, grokBin, and opencodeBin pin the real binaries; init writes them, and TOKENMAXXING_CLAUDE_BIN, TOKENMAXXING_CODEX_BIN, TOKENMAXXING_GROK_BIN, and TOKENMAXXING_OPENCODE_BIN override them for one process, except that init pins the binary it resolved into config.json.
State lives in ~/.config/tokenmaxxing/; outside it the install touches Claude Code's settings.json, codex's hooks.json, the timer and usage hub units, and your shell rc. Each account's credential store is stores/<uuid8>/, which Claude Code reads and refreshes as its own: on macOS the credential is a login-keychain item keyed by the store path (never plaintext on disk), on Linux a 0600 .credentials.json inside the store (the same plaintext model claude itself uses for ~/.claude/.credentials.json).
Codex support
The same pooling works for OpenAI's Codex CLI (your own ChatGPT-subscription accounts):
tokenmaxxing init --codex # log in the first account, isolated + install the codex supervisor & Stop hook
tokenmaxxing add --codex # log in another account, isolated - your primary login is untouched
codex # use codex as alwaysCodex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, so a restart is the switch. Each codex session runs on its own account's store (codex-stores/<uuid8>/, only auth.json per account, everything else shared with ~/.codex so resume works across stores). The installed Stop hook runs the same pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, the weekly aggregate and every named additional limit alike); when it moves, the supervisor relaunches codex resume <session-id> under the target's store with the transcript intact, after compacting the thread on the old account. status (and status --cached) shows every pool.
Two codex-specific facts worth knowing: codex does not run hooks it has not been told to trust, so after init --codex you must open codex once and trust the tokenmaxxing Stop hook via /hooks (auto-switching is inert until then); and each codex session runs on its own store, so no live auth.json is ever shared: tokenmaxxing refreshes a parked account's token only when it reads usage for it, never one a session is running on, and a move is a restart at an idle turn boundary. init --codex refuses a ~/.codex/config.toml that pins cli_auth_credentials_store away from file.
Honest limitations
- A move restarts the process. The session resumes the same transcript under the new account and continues on its own from a first prompt the supervisor submits, but the process restarts; a bar-triggered move compacts the conversation on the old account first, so the first turn on the new account uploads the summary, while a move after a refusal re-uploads the full context once (prompt cache is org-scoped).
- Depleted-pause hiccup. When the whole pool is at its limit,
claudestops for the countdown; anything typed in that split second is lost. - Unsupervised sessions are not moved. A claude started outside the supervisor runs on Claude Code's own login, which tokenmaxxing never writes; when a hard limit lands there, the StopFailure hook prints the shim command that resumes the session under the supervisor.
- One shared identity file.
~/.claude.jsonholds oneoauthAccountthat Claude Code rewrites after whichever session refreshed last, so/statuscan show another session's email; the statusline's◆marks the account a session actually uses. - Keychain (macOS). The first keychain access for a new store happens inside the interactive
init,add, orauthrun, where a prompt can be answered, not inside a headless hook. - Plaintext credentials (Linux). Claude Code itself stores Linux credentials as a 0600 plaintext file; a store follows the same model.
- Status-only pools do not switch. grok
statusshows the weekly subscription credit window; opencode-go accounts still render with no bars. Neither pool moves.
How it's built
TypeScript on Bun. One multi-call entry (src/main.ts) runs the CLI, the claude and codex supervisors, and the hook/statusLine shims. Zod validates every external-boundary payload (credential blobs, hook/statusLine stdin, identity responses, config). es-toolkit for utilities and ky for the few HTTP calls (the identity check, the no-spend usage read, codex's usage and refresh, the npm version read). The supervisor is process/terminal-only. It never proxies API traffic or touches tokens in flight. Cross-process coordination uses flock(2) via bun:ffi (macOS has no flock(1); one codepath for both platforms). Credential I/O goes through one platform-selected store: security(1) generic-passwords on macOS, atomic 0600 file writes on Linux.
License
MIT
