Troubleshooting and FAQ

Common questions and the diagnostics behind them

claude doesn't seem to be supervised. Run tokenmaxxing doctor. The shim must win PATH resolution: doctor checks that the bin directory sits ahead of the real claude on your live PATH and, for bash and zsh, warns about alias claude=... or a claude() shell function shadowing the wrapper, and about aliases that launch a claude binary by absolute path (those bypass the supervisor entirely). Check that the # tokenmaxxing PATH line in your shell rc points at the current bin directory. A session that bypasses the supervisor runs on Claude Code's own login and is never moved.

Auto-switching never fires on Codex. Codex skips hooks it has not been told to trust, and trust is recorded per store path. Open a supervised session on each pooled account, run /hooks, and trust the tokenmaxxing Stop hook there. Trust is recorded against the hook's hash and its position in the Stop list, so it must be re-granted if the hook is ever edited or another Stop hook is inserted ahead of it. tokenmaxxing doctor lists seats still needing trust; it cannot tell a stale hash from a current one. The hook also does nothing in a codex started outside the supervisor.

An account shows a red ✗ in the statusline. The account needs reauthentication: its store's credential is dead (Claude Code cleared it after a refresh came back invalid_grant), the store is empty or unreadable, or five consecutive check-tick reads of it failed. Run tokenmaxxing auth <sel> to reauthenticate it in place; the command tells you which email to sign in with and refuses a login that lands on a different account. The seat's own marker is always ◆; a dead seat shows as ✗ in the other sessions' lines.

Which account does a new session start on? The usable account with the most session-window headroom per running session: (session bar - session used) / (sessions on it + 1), ties by pace pressure. Two fresh accounts alternate; the third session goes to whichever has more headroom left per session. When no account is usable, the session spreads onto the wait assignment instead of the soonest block: the soonest reset with room for another waiter, counting running sessions as well as queued waiters; with no pooled account, or every account flagged, it launches without a store on Claude Code's own login. See How switching decides.

Every account is at its limit and the session waits. With no API key stored, a session waits for the assigned reset when the whole pool is at its limit. To keep working on paid usage instead, store a key with read -rs KEY && printf '%s' "$KEY" | tokenmaxxing key add <label> <credit-usd>; unset KEY, which keeps the key out of the terminal echo and the shell history. Sessions then run on the key until a pooled account is usable again. See API keys.

status shows an API key as refused. A turn on that key ended with billing_error, which Claude Code reports when the key's credit balance is too low. tokenmaxxing moved the session to the next key, or to the wait for a reset when no key is left, and places no session on the refused key. Add credit in the Console, then run tokenmaxxing key credit <label> <usd>, which clears the refusal and restarts the spend count.

Why doesn't it move to the account with the most remaining weekly quota? Weekly quota is the tie-break. The first key is session-window headroom per running session, because the 5-hour window is what a new or moved session drains first; among accounts with equal headroom per session the one furthest behind its own pace wins: remaining percent of its binding gated per-model cap (the Fable cap by default), or of the weekly aggregate when it carries no gated row, divided by time to that reset.

It moved my session and claude restarted. Expected: a session changes account only by respawning under another account's store, at a committed turn boundary, resuming the same conversation. A bar-triggered move first compacts the conversation on the old account, which the supervisor announces on stderr and which can take a few minutes. The statusline flips to the new seat on the first render.

/status shows another account's email. ~/.claude.json holds one oauthAccount for every session and Claude Code rewrites it after whichever session refreshed last. The credential a session actually uses is the one in its store; the statusline's ◆ marks that account.

Does status cost anything? No. status reads the statusline tees and the no-spend usage read; none of them sends a metered request or opens a session window, and status --cached reads nothing but the state files. A live status holds the pool lock while it samples, so prefer --cached while sessions are busy.

An account with running sessions shows no fresh figure. The direct usage read can fail for an account. status then reports usage read failed (see log). An account whose stored access token has expired gets no read with that token, because the endpoint answers such a read with HTTP 401 and, after a few of them, with the hour-long 429 below; the sample first has Claude Code refresh the token (claude -p /usage under that account's store) and reads with the new one. When the refresh leaves the token expired, status reports the stored access token expired and claude did not refresh it (exit <n>), or (exit killed) after the 60-second limit, and a store the refresh cleared is flagged for reauthentication. The usage endpoint also rate limits each account on its own: it answers HTTP 429 with a retry-after time, about an hour in the observed case. tokenmaxxing stores that time on the account (usageRetryAt), and status, the check tick, and the seat's in-decision sample send that account no read until it passes, because a read during the penalty fails too. status then reports the usage endpoint rate limited this account, next read in <n>m. The other accounts are read as usual. The statusline tee stays the primary source for an account with running sessions: tokenmaxxing reads such an account from its tee whenever that feed is at least as new as the stored sample (the row shows the feed's age) and samples the store only when the tee is missing or older and the account's sample interval has passed (see Periodic check); inside the interval the row shows the cached sample and its age, or the reason when the account is rate limited. A failed sample leaves the cached sample in place with its age shown, and the check tick tries the account again once its interval passes.

status says every fable cap is at the weekly bar. Every account that carries a Fable cap has it at the weekly bar, while at least one account still has weekly aggregate headroom for other models. A Fable session has nowhere to move until a cap resets; a Sonnet or Opus session still has room.

Background claude sessions. Claude's background daemon spawns the real versioned binary by absolute path, bypassing PATH shims, so background sessions are unsupervised. A background session spawned from a supervised session inherits its CLAUDE_SECURESTORAGE_CONFIG_DIR and runs on the same account; one started from elsewhere runs on Claude Code's own login. If that process still loads the installed hooks, a hook whose stdin session_id names a session other than the supervised process's live one returns before the decision and writes no respawn marker for the parent. Only a hook run by the supervised claude process itself can change the live session id (/clear, an in-session /resume); see The SessionStart hook.

The supervisor stopped and named a corrupt respawn marker. A marker under respawn/ that fails to parse stops the session instead of guessing whether the pool is depleted. Inspect or delete the file, then run claude --resume <session-id>; a fresh launch clears it.

check failed: self-update from npm failed. The tick's daily self-update failed; the quota evaluation and the sampling it already ran stand, and it retries the next day. See Automatic updates.

Something spawned a lot of processes once. Is that still possible? The supervisor carries layered recursion guards against exactly that historical failure (a stale wrapper pinned as the "real" claude): a depth sentinel in the environment that aborts at 5 levels, an on-disk record of recent wrapper entries that, past 60 entries in 30 seconds, refuses an entry when five of those entries are its process ancestors, which stops a chain whose shim strips that sentinel, realpath identity checks that reject any candidate resolving back into tokenmaxxing's own bin directory, and a hard failure (never a silent PATH scan) when a configured claude path no longer exists. init verifies a candidate actually is claude by running --version before pinning it, and doctor re-verifies the pinned path.

Uninstalling. tokenmaxxing uninstall prints the files and entries it is about to touch, then removes the claude, codex, and pi shims, the tokenmaxxing and xx entry points, the settings entries, the Codex Stop hook, the periodic timer, the usage hub service, and the rc PATH line, and leaves your accounts and their stores intact. It only removes statusline entries it recognizes as its own. On your real login home the command refuses until you pass --yes, so a script or an agent that runs it under a throwaway TOKENMAXXING_HOME cannot strip the live install by accident. Under an isolated HOME the command removes only the unit files under that HOME; the login user's systemctl --user or launchctl instance is addressed only when HOME is the login home.