Claude Account Swap

I run two Claude subscriptions and treat them as one pool. When the one I'm on reaches 95% of its usage limit, every Claude Code chat on my Mac and my PC moves to the other account in the middle of whatever it's doing, subagents included. Nothing restarts. When the first account's window resets, everything moves back. Here's how it works.

The thing that makes it possible

I noticed that running /logininside a busy chat switched accounts instantly, even while subagents were still working. That only makes sense if Claude Code doesn't hold the token in memory for the life of the process. So I tested it: start a headless session, let it pause inside a slow tool call, and change the stored login underneath it.

# a claude -p session, paused in a 30s tool call
[03:15:26] tool call: sleep 30
[03:15:38] >>> stored token overwritten with junk
[03:15:57] api_retry authentication_failed

# same again, swapping in the other account
[03:16:19] tool call: sleep 30
[03:16:32] >>> swapped account A -> account B
[03:16:52] DONE   # finished on account B

Junk token in, the very next request fails. Other account's token in, the session carries on and finishes. Claude Code re-reads its stored login before each API call. Same result on macOS and Windows. So switching accounts for every running session is just a matter of rewriting one credential.

Where the login lives

On macOS it's a keychain item. On Windows and Linux it's a JSON file. Either way the part that matters is the claudeAiOauth object: an access token, a refresh token, and their expiry times. The same entry also holds your MCP server logins, so a swap replaces that one key and leaves the rest alone.

# macOS: the live login is one keychain item
$ security find-generic-password -w \
    -s "Claude Code-credentials" -a "$USER" \
    | jq '.claudeAiOauth | keys'
["accessToken", "expiresAt", "refreshToken", ...]

# another CLAUDE_CONFIG_DIR gets its own item,
# suffixed with sha256(dir)[:8]
$ printf '%s' ~/.claude-backup | shasum -a 256 | cut -c1-8
10abcc5c   # "Claude Code-credentials-10abcc5c"

# Windows and Linux: a plain file
~/.claude/.credentials.json

The swap loop

claude-swap is a single Python script with no dependencies. Once a minute it asks who the live token belongs to, saves that token to a vault, checks usage for both accounts, and swaps if the current one is over the line. Accounts are in priority order, so the primary always wins once it has headroom again. That is what makes it switch back on its own.

# priority order: the primary wins when it has room
ACCOUNTS = ["primary@example.com", "backup@example.com"]
THRESHOLD = 95

def pick(percents):
    """First account under the line, else the least used."""
    for email in ACCOUNTS:
        if (percents.get(email) or 0) < THRESHOLD:
            return email
    return min(ACCOUNTS, key=lambda e: percents.get(e) or 0)

def tick():
    live = live_read()          # keychain, or the JSON file
    oauth = live["claudeAiOauth"]
    active = whoami(oauth)      # GET /api/oauth/profile
    vault_put(active, oauth)    # bank the freshest tokens
    usage = {e: usage_for(e) for e in ACCOUNTS}
    target = pick(usage)        # GET /api/oauth/usage
    if target != active:
        live["claudeAiOauth"] = vault_get(target)
        live_write(live)        # every session follows

It identifies the account from the token itself, not from a state file. If I run /login by hand in some chat, the next tick sees the new owner and files the token under the right account instead of overwriting the other one.

Move tokens, don't copy them

This is the part that bites. A refresh token is replaced every time it's used. If two places hold the same login, whichever refreshes second is logged out. So the script never duplicates a login. The backup account is logged in once in its own config directory, then moved into the vault and removed from there.

# log the backup account in once, in its own dir
$ CLAUDE_CONFIG_DIR=~/.claude-backup claude   # /login

# move (not copy) that login into the vault
$ claude-swap add ~/.claude-backup
vaulted backup@example.com; removed its login
from ~/.claude-backup

The same rule applies across machines. My PC has its own separate logins for both accounts rather than copies of the Mac's. The script also skips a swap if the live token expires within five minutes, so it can't collide with Claude Code writing a freshly refreshed token at the same moment.

Polling without getting rate-limited

Usage comes from /api/oauth/usage, the endpoint behind Claude Code's own /usage. It returns every window (the rolling 5-hour session, the weekly caps) with a percentage and a reset time. My first version checked both accounts every minute and started getting 429s within half an hour. Now the live account is checked every 2 minutes, the idle one every 15, and any 429 pauses that account for 5.

A window whose reset time has passed counts as 0%, even if the last reading was 99%. That means stale data for the idle account is still safe to act on. Each run takes about 0.1 seconds and 16 MB, then exits. Nothing stays resident.

Mac and PC

The same file runs on both machines. On the Mac, launchd starts it every 60 seconds and at login. On the PC, Task Scheduler does the same through pythonw.exe, so no console window flashes every minute. Usage is counted per account, not per machine, so both switch within a couple of minutes of each other.

<!-- ~/Library/LaunchAgents/com.user.claude-swap.plist -->
<dict>
  <key>Label</key><string>com.user.claude-swap</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/python3</string>
    <string>/Users/me/.local/bin/claude-swap</string>
    <string>tick</string>
  </array>
  <key>StartInterval</key><integer>60</integer>
  <key>RunAtLoad</key><true/>
</dict>
# Windows: Task Scheduler + pythonw (no console flash)
$action = New-ScheduledTaskAction `
  -Execute "C:\Python313\pythonw.exe" `
  -Argument "$HOME\.local\bin\claude-swap.py tick"
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) `
  -RepetitionInterval (New-TimeSpan -Minutes 1)
Register-ScheduledTask -TaskName claude-swap `
  -Action $action -Trigger $trigger

Day to day

I mostly forget it exists. A notification pops up when a switch happens, and every switch is logged with both accounts' usage at that moment. When I want to check, it's one command.

$ claude-swap status
* primary@example.com  live=20%  [session 20%]  0m ago
  backup@example.com   live=41%  [session 41%]  14m ago

active: primary@example.com; would pick: primary

$ tail -2 ~/.claude-swap/swap.log
08:49 switched -> backup (primary 95%, backup 11%)
13:10 switched -> primary (backup 38%, primary 0%)

Limits

  1. Up to about two minutes of lag. A burst of parallel subagents can push past 95% before the next check.
  2. Apps that display the signed-in email may keep showing the old account after a swap. Billing follows the token, not the label.
  3. It relies on undocumented behaviour: the re-read on each request and the usage endpoint. A Claude Code update could change either, so the script fails safe and simply stops swapping if a call breaks.