Account switching and quota dashboard for Claude Code.
A single local executable that serves a loopback web page showing every one of your Claude Max accounts with its 5-hour and 7-day headroom, ranks them by earliest weekly reset, and switches the whole machine to the account you pick, with no browser step. Every open Claude Code session follows the switch on its next request.
Status: a tagged release exists.
- Nothing calls the model API outside the unmodified
claudebinary. - Credential pairs are moved between your own files on your own machine, never copied, so each refresh token exists in exactly one place. Handing an account to the other side of the same machine (a WSL distribution) crosses a volume, where a rename does not exist: there the pair is staged, verified by fingerprint, promoted and deleted under a journal, so at most one reachable copy exists at every instant. Nothing is ever copied to another machine.
- CI tokens never leave memory. An account's Console API key is the exception: you paste it once per machine, and it is kept only in the OS store (Windows Credential Manager, persisted for this computer only, or the Secret Service through libsecret on Linux). The tool never writes it to a file of its own, a log or a page, and never copies it to another machine. The key belongs to your user on this machine, so setting or removing it needs your interactive sign-in: an SSH or service session has no credential store (Windows error 1312). With no usable store, setting a key is refused with the cause. A store that does not answer within 15 s is reported as a failure, but the write or delete may still complete.
- Removing an account, or its key, deletes the stored key. If the roster records a key and the
store cannot be reached or refuses, the removal is refused with the cause and the account stays
on the roster with its key record (a removed account's login and folder are already handled);
?keepStoredKey=truegoes ahead and names the key left behind and the command that removes it (cmdkey /delete:<target>on Windows; on Linux, unlock the keyring first, sincesecret-tool clearremoves only unlocked items). - The tool identifies itself honestly on every request it makes.
- Every switch is a human click; nothing rotates on its own. Usage alerts never act: they only say that an account backing a CI role is near the end of its 7-day window.
- The tool never calls the model API.
- It reads the undocumented usage endpoint with its own User-Agent, which the research rates GRAY
(no Anthropic statement either way), not ALLOWED. It reads only when the dashboard is opened,
Refresh is pressed, or the rate-limit stop hook fires, at most once a minute per account, never
on a timer. It reads GitHub only through
gh api, with gh's own User-Agent and sign-in, and only when you click Check roles, Generate CI token, Push API key, or a kill switch's Stop or Start: those read secret names, dates and visibility, never a secret's value, and the kill switches' values. Stop or Start reads the variable's visibility first and reads the switch back after the write. The limit-stop check before a role moves makes no GitHub call. The first rate-limit answer stops what was clicked, and nothing retries it. Writes go one at a time, at least a second apart. It changes a lane kill switch's value only when you confirm its toggle, and never creates one. - The parked-pair refresh presents Claude Code's public OAuth
client_id. - An optional Admin key, stored per account like the Console key, lets Check API key read the
organization's key list from the Admin API (
GET /v1/organizations/api_keys) and say whether the stored Console key is archived, inactive, expired or near its expiry. This is not a model API call: it reads key metadata, only on that click, with the tool's own User-Agent, and the first rate-limit answer ends it. It finds the key by the list's partial hint and says "cannot tell" when the hint is missing or fits more than one key. Individual accounts have no Admin API. - Open Console, in the Set API key and Set Admin key panels, opens the Console page where that
kind of key is created in the account's mapped browser profile. Open claude.ai and Open Console,
in a card's ··· menu, open claude.ai and the Console's API keys page there. Each only starts the
browser: it reads nothing from GitHub or Anthropic, and it opens only the three fixed pages the
tool holds (
AnthropicEndpoints.AccountPages). An account not on the roster, no mapped browser, or a failed launch is a 409 with the reason; a bad page name or email is a 400. - Running the loop lanes (
work-loop,babysit-loop,attend-queue) while rotating accounts is outside V1 because reader-side invalidation of a latched window is still an open problem.
Last reviewed: 2026-10-05.
This is the maintainer's reading of public sources, not legal advice. Anthropic "reserves the right to take measures to enforce these restrictions and may do so without prior notice" (legal and compliance), and it can change its terms at any time.
| Source | URL | Effective or checked |
|---|---|---|
| Consumer Terms | https://www.anthropic.com/legal/consumer-terms | effective 2025-10-08, checked 2026-10-05 |
| Commercial Terms | https://www.anthropic.com/legal/commercial-terms | effective 2025-06-17, checked 2026-10-05 |
| Usage Policy | https://www.anthropic.com/legal/aup | effective 2025-09-15, checked 2026-10-05 |
| Claude Code legal and compliance | https://code.claude.com/docs/en/legal-and-compliance | checked 2026-10-05 |
| Claude Code authentication | https://code.claude.com/docs/en/authentication | checked 2026-10-05 |
| Claude Code GitHub Actions | https://code.claude.com/docs/en/github-actions | checked 2026-10-05 |
| Claude Code desktop app | https://code.claude.com/docs/en/desktop | checked 2026-10-06 |
| Log in to your Claude account | https://support.claude.com/en/articles/13189465-log-in-to-your-claude-account | checked 2026-10-06 |
| Supported countries | https://www.anthropic.com/supported-countries | checked 2026-10-05 |
| Safeguards warnings and appeals | https://support.claude.com/en/articles/8241253-safeguards-warnings-and-appeals | checked 2026-10-05 |
- Consumer Terms §2: "You may not share your Account login information, Anthropic API key, or Account credentials with anyone else." and "You also may not make your Account available to anyone else."
- Consumer Terms §3 forbids access "through automated or non-human means, whether through a bot, script, or otherwise", "Except when you are accessing our Services via an Anthropic API Key or where we otherwise explicitly permit it".
- Legal and compliance: "Advertised usage limits for Pro and Max plans assume ordinary, individual usage of Claude Code and the Agent SDK."
- Legal and compliance: "developers may not collect, store, or intermediate Claude.ai credentials or session tokens — sign-in to a Claude account must complete through Anthropic's own flow".
- Legal and compliance: the restriction does not "prevent an end user from signing in to the unmodified Claude Code binary with their own Claude subscription".
- Usage Policy: no coordinating "malicious activity across multiple accounts to avoid detection or circumvent product guardrails", and no circumventing "a ban through the use of a different account".
- Authentication docs: "To stay signed in to multiple accounts at once, such as work and personal accounts, give each account its own configuration directory."
- GitHub Actions docs: a
CLAUDE_CODE_OAUTH_TOKENgenerated withclaude setup-tokenworks on Pro, Max, Team and Enterprise plans. For a secret shared across repositories the docs advise an API key instead, "since an OAuth token is tied to the subscription of the person who ranclaude setup-token." - No term in these sources limits how many subscriptions one person may hold, and none names usage-limit or rate-limit circumvention.
- Every account in the roster belongs to you and is paid by you. Never share, lend or pool accounts or credentials: Consumer Terms §2.
- Keep Team and Enterprise seats out of the roster; the tool refuses them
(
MaxTierAdmission.cs), and they fall under the Commercial Terms. - Use only the unmodified
claudebinary for model requests, with no proxy and no third-party harness on subscription credentials: the legal page allows signing in to "the unmodified Claude Code binary". - Sign in through Claude Code's own login flow (
claude auth login), never by pasting tokens taken from somewhere else: sign-in "must complete through Anthropic's own flow" (legal page). - Switch by hand. Do not add automatic rotation at usage limits: limits "assume ordinary, individual usage" (legal page); see the human-click bullet under Posture.
- Keep the tool's own User-Agent and never imitate Claude Code's: Anthropic has banned tools that spoofed the Claude Code client (public report).
- Read usage only when asked, inside the existing budget: the usage endpoint is undocumented; see the read-budget bullet under Posture.
- CI: role secrets are organization secrets visible to all of the organization's repositories,
on purpose: every lane in the organization reads its role's pair. Each account you own makes one
claude setup-tokentoken, which goes into every role that account backs, and the lane launcher spends the role's API key first and falls back to that token (run-claude-cli). Paste each account's Console API key once per machine; it stays in the OS store. Re-read the GitHub Actions docs, which advise an API key for a secret shared across repositories: the token "is tied to the subscription of the person who ranclaude setup-token". - Use Claude only from a supported country, and avoid VPN exits that change your region: account creation from an unsupported location is a listed suspension reason.
- Re-check the sources table whenever Anthropic announces a terms, limits or enforcement change, and at least once a quarter, then update the "Last reviewed" date above.
| Behavior | Does it? | Where |
|---|---|---|
Model inference outside claude |
No | Posture, first bullet |
Proxy between claude and Anthropic |
No | Posture, first bullet |
| Background timer for reads, refreshes or switches | No | Posture, switch and read bullets; QuotaRefresh.cs |
| Refreshes a parked token | Only during a requested read: when its recorded expiry has passed, or once when the usage endpoint answers 401, followed by one retried read. Done under the tool's own write gate; a switch holds Claude Code's own refresh lock | QuotaRefresh.cs, OAuthRefreshLock.cs |
| Usage reads | Bounded: at most once a minute per account, only on request | Posture, read bullet; RefreshBudget.cs |
| Usage alerts | Information only, never an action | Usage alerts |
| User-Agent | Its own on every Anthropic request; GitHub reads and writes go through gh, with gh's own |
AnthropicEndpoints.cs, GhProcess.cs |
| Admin API reads | Only on Check API key, with a stored Admin key: the organization's key list, metadata only, never a model call | Posture; AnthropicAdminKeysClient.cs |
OAuth client_id for parked refreshes |
Claude Code's public one | Posture; AnthropicEndpoints.cs |
| CI tokens | Only on a click: the unmodified claude setup-token makes the token, you sign in on Anthropic's page and paste the code, and the token goes from the CLI's output to gh secret set in memory, never to disk, a log, or the page |
Assigning accounts to CI roles; ClaudeCliSetupTokenRunner.cs |
| Console API keys | Kept only in the OS store, per machine; read only to hand to gh secret set on its standard input |
Posture; ApiKeyEndpoints.cs |
| GitHub reads | Only on a click, through gh api: secret names, dates and visibility, never a secret's value, and the kill switches' values; on a kill switch's Stop or Start, its visibility first and the switch read back after; the limit-stop check reads nothing from GitHub |
Posture; GhApiReader.cs |
Two points are gray:
- The usage endpoint is undocumented. No Anthropic statement allows or forbids reading it.
- The legal page says "developers may not
collect, store, or intermediate Claude.ai credentials or session tokens". The tool stores and
moves your own tokens between your own folders, and refreshes parked ones with Claude Code's
client_id. That sentence is addressed to "developers" building products that offer Claude sign-in to their users, which this tool does not do, but the page does not address a person managing their own credentials.
- Anthropic confirmed in January 2026 that it banned use of third-party tools that spoofed the Claude Code client: https://x.com/trq212/status/2009689809875591565.
- Account creation from an unsupported location is a listed reason for suspension: safeguards warnings and appeals.
- Unconfirmed anecdote, not confirmed by Anthropic: community reports link bans to automatic multi-account rotation that refreshes tokens in the background and polls usage for idle accounts: https://www.reddit.com/r/ClaudeCode/comments/1uouvo8/ (2026-07-06).
Last checked: 2026-10-06.
The tool switches the Claude Code CLI only. A switch leaves the Claude Desktop app, including its Code tab, on whatever account it last signed in to. To change Desktop's account, sign out and back in from Desktop itself.
What Desktop does:
- It has its own login, not the CLI's. The authentication docs say "Claude Desktop and cloud
sessions do not call
apiKeyHelperor read these environment variables: they use OAuth" (authentication). Desktop and the CLI share settings, MCP servers andCLAUDE.md, and the docs list no shared login (desktop). A request to share the CLI's login with Desktop was closed as not planned (anthropics/claude-code#62206). - Its Code tab counts against Desktop's account. Desktop's usage ring shows "your plan usage for the period", and "plan usage is shared across all your Claude Code surfaces" (desktop).
- Anthropic documents one account switch only. An individual account and a Team or Enterprise account on the same email can be switched from the initials menu (log in to your Claude account). Desktop has no documented counterpart to the CLI's per-account configuration directory.
Community tools take two approaches:
- Swapping Desktop's files. These tools save and restore Desktop's stored login while it is
closed: its cookie file, which holds the claude.ai web session, and the sign-in token entries in
its
config.json. Some also decrypt those tokens to read usage on a timer, or switch accounts automatically. - One data folder per account. These tools start the official app with Chromium's
--user-data-dirlaunch option, so each account signs in and refreshes inside its own folder and the tool never touches a credential. Anthropic does not document the option for Desktop. An open report in its tracker says a sign-in started in a second instance completes in the first one (anthropics/claude-code#98549).
Why the tool does neither:
- Swapping files parks more than the CLI does. Moving the CLI's credential files is already gray (see How this tool compares). Desktop's cookie is a full claude.ai web session, the "Claude.ai credentials or session tokens" the legal page names, and its storage layout is undocumented and can change with any Desktop update.
- A data folder per account rests on an undocumented launch option. It keeps credentials inside the official app, so it is the lower-risk route, but it has no Anthropic statement behind it and has open sign-in bugs. It stays out of scope until Anthropic documents it.
- No public report ties a ban to either approach, and no Anthropic source allows either. The absence of reports is weak evidence: these tools have few users.
Download the release from
https://github.com/melodic-software/claude-code-account-rotation/releases/latest.
The assets are claude-code-account-rotation-win-x64.exe (the Windows leader),
claude-code-account-rotation-linux-x64 (the follower the WSL side runs),
claude-code-account-rotation-follower-log (the wrapper the leader starts in its
place), SHA256SUMS, and config.template.json (the template the executable embeds).
Check each binary against the digest SHA256SUMS lists for it. A release download does not keep
the executable bit, so on Linux run chmod +x on claude-code-account-rotation-linux-x64 and
claude-code-account-rotation-follower-log before the first run. Place the executable on PATH,
or make a shortcut to it, and run it once.
A missing config.json is written on that first run from the embedded template. The written file
fills its paths from the user profile. It sits in the app data directory unless --config pointed
somewhere else. That directory is %LOCALAPPDATA%\claude-code-account-rotation on Windows, and
claude-code-account-rotation under the local application data directory (~/.local/share unless
XDG_DATA_HOME is set) on Linux and macOS.
The process prints Dashboard: and the bound URL. The default port is 48211. --port 0 asks the
operating system for a free port for that launch. Without --open no browser opens: open the
printed URL, and paste line 2 of instance.url when the page asks for the instance token.
With --open, the dashboard opens in the default browser already signed in. If an instance is
already running, --open opens that instance's dashboard and exits. Otherwise, on Windows,
--open starts the leader as a separate process with no window, waits up to 30 seconds for it
to listen, opens its dashboard, and exits. If it does not come up, --open exits non-zero and
says why; run the same command without --open to watch it start in the console. On Linux and
macOS, --open with no instance running starts the process in the foreground as usual and opens
its own dashboard once it is listening.
The leader --open starts has its own hidden console, so closing an ordinary terminal window
does not stop it. A launcher running inside a job object that kills its processes on close still
takes the leader down with it; some IDE terminals and Win32-OpenSSH sessions do that. The
Start-menu shortcut and the logon task are not affected.
Stop the tool with the Stop button in the dashboard header. It stops the WSL follower first and
then the leader, and lets a rotation in flight finish. While a switch or import is in progress on
either side, it refuses, says why, and both keep running. That refusal is the only thing that
blocks the stop: any other follower answer, or none within 10 s, lets the leader stop, and the
page shows what the follower said. With more than one side in peers[] it refuses before
stopping anything; stop each process with POST /api/shutdown instead. The page cannot start
the tool again: run claude-code-account-rotation --open, or on Windows use the Start-menu
shortcut.
When the leader starts, it brings up every side that has peers[].launch set. It leaves a side
that is on its own version alone. It starts a side that is not running. A side on another
version is stopped and started again. It moves no account. If a side cannot be brought up, the
leader logs why and keeps running, and the side reads offline or incompatible. Start WSL side
on the page tries again. Upgrading a release says how this lets either
side be installed first.
Scripts use routes. POST /api/shutdown stops only the process it is sent to, on either side,
which is what an upgrade that replaces one side's binary wants. POST /api/stop on the leader
is the Stop button: send it in place of /api/shutdown below to stop both sides. Both answer 409
while a switch or import is in progress:
$instance = Get-Content "$env:LOCALAPPDATA\claude-code-account-rotation\instance.url"
Invoke-RestMethod -Method Post -Uri "$($instance[0])/api/shutdown" `
-Headers @{ Authorization = "Bearer $($instance[1])"; 'X-Claude-Code-Account-Rotation' = '1' }Stop-Process -Name claude-code-account-rotation or Task Manager is a last resort: a hard kill
skips that drain, and a token refresh cut off mid-write can leave an account's rotated
credentials stranded.
If a card says it is not on the roster, click Adopt. Adopt is only offered for a Max account
that is already logged in on this machine. Otherwise use Add an account, then Login on the card
that needs a login. profilesRoot is the only hand edit, and the tool has to be restarted after
it.
When a session stops on a usage limit, Claude Code waits for the reset. The optional hook
hooks/rate-limit-stop.sh tells the running dashboard, which reads the live account's usage once
(at most once a minute per account). When a side's account is at 100% of its 5-hour or 7-day
window, or of a model's weekly window such as Fable's, the dashboard shows a "switch now" prompt
naming the first usable account in its order whose usage was read in the last 30 minutes. An
account at its Fable weekly limit is never named, because the tool cannot know which model a
session will run; you can still switch to it by hand. When every usable account's figures are
older than that, the prompt names none: it says which account is unverified and how old its
figures are, and offers Refresh usage. Nothing switches until you click it.
Install it on the side the leader runs on. The follower has no hook route, and the hook reads only the account the leader's side is logged in as. For an account the other side holds, Refresh usage asks that side to read it with its own access token, and its card also shows that side's statusline figures.
-
Save
hooks/rate-limit-stop.shfrom the release tag you run, and on Linux runchmod +xon it. It needsbashandcurl. On Windows, Git Bash is required: without it Claude Code runs hooks in PowerShell, wherebashcan resolve to WSL'sbash.exeand the hook does nothing. -
Add the hook to
~/.claude/settings.json, with the path where you saved the script (Claude Code 2.1.78 or later):{ "hooks": { "StopFailure": [ { "matcher": "rate_limit", "hooks": [{ "type": "command", "command": "bash /path/to/rate-limit-stop.sh", "timeout": 10 }] } ] } }
The script reads instance.url from the default app data directory and exits quietly when the
app is not running. It posts to POST /api/hooks/rate-limit with the headers POST /api/shutdown
takes. It answers 202 when a read started, or 409 with one of these refusal values:
NoLiveAccount (no account is logged in on this side), RateLimited (the account was read inside
the last minute, or the usage endpoint's lockout is still running), or RefreshInProgress (a
refresh is already running).
An account that backs a CI role spends its 7-day window on CI runs as well as on your sessions,
often while nobody is watching its card. When such an account's 7-day figure reaches the warning
threshold, a line above the cards says so; from the critical threshold the line is red. Both
thresholds are set in UsageAlerts.cs.
Each CI-role account's 7-day meter carries a tick at each threshold, and its card has a Re-issue
token button beside the meters. The button opens the Generate CI token panel with that account's
roles already ticked. To move a role off the account instead, open Generate CI token on the
account that is to take it.
The alerts are computed at the end of a full refresh pass, which the page starts when it opens and when you click Refresh all. The 10-second poll only shows the last result and its time, and a card's own Refresh usage leaves the alerts as they were. They use the figures the cards show, so a window that has reset since it was read raises nothing. No alert switches an account, moves a role, or sends anything.
A Remote Control session belongs to the account signed in when it started, and Claude Code stops
it once that account changes; nothing restarts it
(docs).
So when you click Switch for the Windows side and the scheduled task named by remoteControlTask
(default claude-remote-control) is running, the dashboard first asks:
"Switch to X? Remote Control restarts under X; live Remote Control sessions on this machine drop
and reopen in X's Claude app." After the switch, and once claude auth status reports the new
account, the tool restarts the task:
- It records the task's process tree: the process whose command line carries the task action's
arguments, and everything under it (
conhost.exe,pwsh.exe, then theclaude.exeserver). Aclaude remote-controlyou started in a terminal is not under that process and is not touched while the terminal runs. Aclaude.exewith exactly the task'sremote-controlarguments whose parent has exited is taken for the task's server left behind and is recorded too, even if a terminal you have since closed started it; one whose parent is still running is not. - It ends the task. That ends only the root process, so it then ends the recorded processes
still running,
claude.exefirst. A task that had already stopped is not ended, but a server it left behind still is. - It waits up to 15 seconds for the task to stop, then runs it again.
- A task that was not running is left alone, and the switch asks nothing, unless a server it
left behind (as in step 1) is still running: then the switch asks, ends that server, and runs
the task. When the page cannot
read the task's state, it still asks, as a condition. The state comes from
Get-ScheduledTask, whose state is a number, so it reads the same in every display language. - A task whose state could not be read before the switch is restarted only if its processes are found; one found stopped is not started, and the result says so.
- A second switch sent while the first one's restart is running is refused (
MutationInProgress). - A switch of the WSL side, and any switch on Linux, restarts nothing.
- A restart that fails never fails the switch. A process that cannot be ended stops the restart
before the task runs again, so two servers never run. The leader's log records each restart,
each server ended after its parent exited, and why a restart failed. A running task whose processes cannot be
identified is neither ended nor run. The result says why and what to run by
hand:
Stop-ScheduledTask claude-remote-control, end theclaude.exerunningremote-controlthat it leaves behind, thenStart-ScheduledTask claude-remote-control. remoteControlTasknames a task in the Task Scheduler root folder; a path or a wildcard (\ / * ? [ ]) is refused at startup.- Set
remoteControlTasktonullor""inconfig.jsonto turn this off. A file without the key uses the default.
GET /api/dashboard, sent with Authorization: Bearer and the token on the second line of
instance.url, returns more than the page needs. Only these fields are supported for scripts;
a change to any of them is a breaking change in the CHANGELOG. Everything else may change
without notice.
accounts[], in the dashboard's order: groupedusable, thenmodel-limited, thenlimited, thenexhausted, thenunread, thenpaused. Within a group, accounts sort bynextResetAt(the weekly reset for a usable account), earliest first, with an undated one after every dated one, then by e-mail.accounts[].email.accounts[].standing:usable,model-limited(a model's weekly window, such as Fable's, is at 100% and the 5-hour and 7-day are not;nextResetAtis that window's reset andaccounts[].limitedModelnames it),limited(the 5-hour window is at 100% and the 7-day is not;nextResetAtis the 5-hour reset),exhausted(the 7-day window is at 100%),unread, orpaused.accounts[].nextResetAt: when the account frees up (ISO 8601), or null when there is no wait or none that can be dated.accounts[].usage.limits[]wherekindissession(5-hour) orweekly_all(7-day):percent(null when unknown or the window has reset since the read) andresetsAt.accounts[].isLive: true for the account this side is logged in as.accounts[].slot: where the account's pair is when the store is shared with another side:held-here,held-elsewhere,parked,in-transit, ornever-logged-in; null when the store is not shared.
Which side holds a held-elsewhere account comes from GET /api/sides, sent the same way. It
returns one entry per configured side (an empty list when none is). Supported fields:
[].side: the side's name, for examplewsl.[].online: whether that side answered.[].liveAccount: the account that side is logged in as, or null when it holds none. It is also null when the side did not answer, and that side may then still hold an account.
GitHub Actions lanes run Claude Code on a funding role's secrets. A role is a prefix plus its
organization: its token secret is <PREFIX>_CLAUDE_CODE_OAUTH_TOKEN and its key secret
<PREFIX>_CLAUDE_API_KEY. The bare CLAUDE_CODE_OAUTH_TOKEN is the legacy (review lanes) role,
with a token secret only, and any other name the roster kept from an earlier release is a role
with a token secret only. A role is keyed (its key half is written and pushed) as the lane
catalog says, below; with no catalog configured, every role with a key half is keyed except
REVIEWS, REVIEWS_SECURITY and ISSUE_LANE, whose lanes run on claude-code-action and read no
key, so the tool neither asks for nor writes a key for them. The lane launcher spends a role's API
key first and falls back to its token. An organization role's new secrets are created visible to
all repositories, on purpose; existing ones keep their visibility.
The tool never writes a secret to one repository, whatever its name, and no option or setting changes that: every secret it sets is a CI credential, set on an organization. A role in a repository that an earlier release recorded is still read and shown; Generate CI token refuses it and Push API key skips it.
The lane catalog is a JSON file in a repository, named in config.json as
"laneCatalog": {"repository": "owner/name", "path": ".github/lane-funding.json", "ref": "main"}
(ref is optional and defaults to the repository's default branch; absent or null means no
catalog). It lists the organization and each role's secretPrefix, tokenSecret, keySecret,
keyed flag and lanes, each lane with the currentSecrets it reads today. A role is keyed when
the catalog says "keyed": true, or when any lane in the catalog still lists its key secret in
currentSecrets, so a key a lane reads keeps being refreshed until that lane moves off it. The
tool reads the catalog with one gh api call, through the contents API, on Load lane catalog in
the Generate CI token panel or on the first Generate CI token or Push API key that needs it, and
holds it until the next load. With a catalog read, the panel lists its roles and hides the
free-text row. A configured catalog that cannot be read refuses Generate CI token and Push API
key with the reason rather than guessing which roles are keyed. GET /api/lane-catalog returns
what is held, with no read; POST /api/lane-catalog/refresh reads it again.
One account backs each role half, and an account can back several roles. Both halves normally
come from the same account: the token from that account's claude setup-token, the key from that
account's Console API key, which you paste once per machine with Set API key and which stays in
the OS store (see Posture); when one half fails to send, the role is split (below). A
card shows one badge per role it backs, such as automations · token+key (hover it for the
secret names and dates). A token badge warns in the last 30 days of the token's one year and says
when it has expired, and the card lists, per expiry date, which roles fall back to their API key
only (any account holds the key half) and which stop. CI draws on that account's 5-hour and 7-day
limits alongside your interactive sessions on it.
Prerequisite: the GitHub CLI (gh) on PATH, signed in (gh auth login) as someone who may write
those secrets (admin:org for organization secrets). The tool checks this before a token is made.
-
Pick the card with the most 7-day headroom. If any role you will pick has a key half, store the account's Console API key first (··· menu, Set API key). Before you make the key, turn the account's auto-reload (Console) and extra usage (claude.ai) off. Open Console, in that panel, opens the Console's API keys page in the account's mapped browser profile, so you create the key signed in as that account; name it
github-actions(the Set Admin key panel opens the Admin keys page). The panel lists the keyed roles, the catalog's when one is read, each with the account that sets its key now. Tick the roles the key is to back, answer Auto-reload and Extra usage, paste the key, and choose Save and send. The key is stored (Credential Manager on Windows, the Secret Service on Linux) and sent to each ticked role's key secret, and that key half moves off the account that set it before, after the limit-stop check below. With a key already stored, leave the field blank to send the stored one; Push API key opens the same panel. With no role ticked, Save only stores the key.The key is sent only when you answer that auto-reload and extra usage are both off. No API this tool can call reads auto-reload, so it asks on every write that sends a key (this panel, and Generate CI token for a keyed role) and sends nothing on a missing answer or an
On. When the account's last usage read says extra usage is on, it refuses whatever you answered; turn it off, refresh the account's usage, and try again. The card's roster records the day you last confirmed both off. -
Open its ··· menu, then Generate CI token. Tick the roles it is to back, and for a keyed role answer Auto-reload and Extra usage as above. With a lane catalog read, the list holds the catalog's roles, then any other role an account backs. Without one, it holds every role an account backs and, after Check roles, every role the organization's secrets name; when no role is known yet, the panel offers Check roles itself and redraws the list from what it finds, and to create a role you type its prefix and organization, and the page asks before it creates the secrets. Choose Start sign-in: the button names the browser and profile it opens, or says you will get a link when no browser is mapped.
-
Anthropic's sign-in page opens in the account's mapped browser profile, with the account's address in its email box when the profile is signed out. Sign in as that account, then paste the code it shows into the card and choose Submit code.
-
The tool sets each role's token secret from the new token, then its key secret from the stored key, one at a time and at least a second apart. Before any token is made it reads who may see each existing secret and passes that back (
--visibility all,private, or--reposwith the selected repositories), sincegh secret setotherwise makes a secret private; a new secret is visible to all repositories. Each half set from this account is taken off the account that backed it.
The token and key never leave memory: they are not shown, logged, written to disk, or passed as an
argument. A failure before any secret is set (a rejected code, gh refusing every write, or a CLI
release whose screen the tool cannot read) ends the session without storing anything, and the
message gives the manual commands: claude setup-token, then gh secret set <name> --org <org>
with the secret's current visibility (--visibility all, --visibility private, or --repos a,b
for selected repositories), since gh otherwise makes it private. When some halves are set and others are not, the roster records the halves that were
set and the message names each one that was not. A role whose token was set but whose key failed,
or whose key was set but whose token failed, is split (token from one account, key from
another): the account that set the half that failed keeps it, and both cards say split and name
both accounts until that half is re-sent. Every claim moves only the halves it sets. Push API key,
in the ··· menu, sends the stored key to the key secret of the roles you tick, with no sign-in.
Use it after replacing a Console key too. The first rate-limit answer from GitHub stops every
write after it, and nothing retries it.
The same start is POST /api/accounts/{email}/ci-token with {"roles":[{"prefix":"PR_VERIFY", "organization":"octo"}],"costSettings":{"autoReload":"off","extraUsage":"off"}} (tokenSecret
in place of prefix for a token-only role, which needs no costSettings). The 2.1.0
body, one secretName with its organization, visibility and repositories, still works: it
writes the token half only, to that one secret, and refuses a repository. With an organization and no visibility, it keeps the visibility the secret has;
a secret that does not exist yet is created visible to all repositories when its name ends in
CLAUDE_CODE_OAUTH_TOKEN (a role's), and private otherwise, as gh's default made it.
Push is POST /api/accounts/{email}/api-key/push with the same costSettings and an optional
roles list in the same shape: keyed roles only, and only roles the catalog lists when one is
configured. With no roles, it sends the key to every keyed role the account backs.
claude setup-token draws its screen only when its input is a terminal, so the tool runs it
under one: a pseudoconsole (ConPTY, Windows 10 1809 or later) on Windows, and a POSIX
pseudo-terminal on Linux. On macOS the action refuses and you run the commands by hand. The
terminal is 1,000 columns wide, so nothing on it wraps. The tool reads only two things from that
screen: the sign-in URL, which must be whole (with its code_challenge and state), and the
token, matched by its sk-ant-oat01- prefix. Exactly one token must appear, standing alone on
its row. CI runs the real CLI under that host on both Windows and Linux, so a release that
changes either one fails a check.
Check roles, in the header, lists the Actions secrets of every organization a role on the roster
names, and of one you type, with gh api (the admin:org scope reads them). It lists each role
the secret names imply, with the account that backs it, and judges each half the roster records:
ok; changed outside the tool when GitHub's updated_at is more than 2 minutes after the
tool's write, or, for a half recorded with a date only, on a later day in this machine's time
zone, where the tool records those dates; unknown for such a half updated that same day, or
one GitHub lists as older than the tool's write or with no readable time; and missing when the
secret is gone. A role whose secrets exist but that no account backs is unclaimed. It reads
only on that click, and the first rate-limit answer ends the check with no retry.
The same click reads the lane kill switches: the organization variables CLAUDE_LANES_DISABLED,
CLAUDE_REVIEW_DISABLED and CLAUDE_SECURITY_REVIEW_DISABLED of each organization it read, and
CLAUDE_LANES_REPO_DISABLED of each repository listed in laneRepositories in config.json
(["owner/name", ...]; absent means none). CLAUDE_LANES_DISABLED and
CLAUDE_LANES_REPO_DISABLED show running only when exactly false, and stopped on any other
value or when the variable does not exist. The two review switches show stopped only when exactly
true. Values compare exactly, so False or false is not false. Each organization switch
has a Stop or Start button that asks first. It then reads the variable's visibility (and, when it
is selected, the ids of its repositories), runs gh api --method PATCH orgs/<org>/actions/variables/<name> -f value=<true|false> -f visibility=<as read> (plus
-F selected_repository_ids[]=<id> per id), and reads the switch back. GitHub's documentation
marks visibility optional but does not promise an omitted one is kept, so the tool sends back the
visibility it just read. If that read fails, nothing is written. A variable GitHub answers 404
for is reported as not set or not visible to this sign-in, and never created. The repository
switch is read-only, and no repository or environment variable is ever written. The route is
POST /api/kill-switches with {"org", "name", "value"}; any other name, or a value other than
true or false, is refused before gh runs.
Before Start sign-in or Save and send moves a role off other accounts, the page checks those accounts' usage from the last dashboard, with no request: the 5-hour and 7-day figures the claude binary reads for each account. When one of them is at 90% or more (the meters' red tier), the panel names the account, the figure and when it resets, with Move anyway and Cancel; it never blocks the move. An account with no usage read yet is named as not checked. A Console key's credit stop has no such figure, so the panel says credit stops are not checked. Otherwise the move starts at once.
To mark an account by hand instead (for a token made outside the tool, or when the tool set the
secret but could not update the roster), open Edit and set "CI token generated on". Optionally
also open "Repair a role record", folded under the role list, and set "CI secret name" and "CI
secret in" (owner/repository, or an organization), then Save; that adds the secret's role with its token set on that date, taking that token half off the
account that backed it before; the other half stays. That is the only role Edit gives an account,
and it is refused (409) when another account holds that token half set on a later day, or set
through the tool on the same day: that half is newer than the token you are recording, so run
Generate CI token instead.
Untick a role in Edit to take it off this account only. Edit sends the date only when you change
it and names only the roles you untick, so a form opened before another account took a role
cannot take it back. The same writes are PATCH /api/accounts/{email}, sent with the headers
POST /api/shutdown takes, with
{"ciTokenGeneratedOn":"yyyy-MM-dd","ciTokenSecret":{"name":"...","repository":"owner/name"}}
(or "organization" in place of "repository") to add one role, and with
"removeCiRoles":[{"prefix":"AUTOMATIONS","organization":"octo"}] (tokenSecret in place of
prefix for a token-only role, repository in place of organization) to take roles off. The
older "ciRoles", the account's whole list with each half's tokenSetOn and keySetOn
(yyyy-MM-dd) and their instants, can only leave out roles or halves: naming a half the account
does not hold, with the same dates and instants, is refused with 409. A marker set in Edit that
names no secret is a hand marker: hand markers keep replacing each other, so at most one account
carries one, and a role claim never clears one. Clearing the date clears every token half. On an
account that backs a token half, the date is always that of its newest token half, whatever the
request sends, so taking off the newest role lowers it and taking off the last one clears it.
Every change of which account backs a role half is appended to lane-history.jsonl under the app
data directory: one JSON line with the time, the role, token or key, the account and the
previous account (or null), and the route (assign, push, edit or delete). It holds names
and dates only, never a secret value, and a line that does not parse is skipped.
GET /api/lane-history?since=<date or ISO time> returns the entries, and the page's History
disclosure reads the last 30 days when you open it, one line per entry naming the role and its
organization or repository.
Requires the .NET SDK pinned in global.json.
dotnet restore --locked-mode
dotnet build -c Release --no-restore
dotnet test -c Release --no-buildA fix pull request that should ship dates its CHANGELOG heading: ## [x.y.z] - YYYY-MM-DD.
Ask the operator before dating a heading. When that change reaches main, the release workflow
tags the merge commit vx.y.z and publishes the release in the same run. A push to main whose
top dated heading already has a published release publishes nothing. Pushing a v* tag by hand
still publishes from that tag.
The tool was named account-rotation until 2026-09-07. The executable, the User-Agent product
token, and the per-user app data directory carry the new name, and the tool does not migrate the
old directory. Stop the old executable, then move the directory once, contents unchanged:
- Windows:
%LOCALAPPDATA%\account-rotationto%LOCALAPPDATA%\claude-code-account-rotation. - Linux and macOS:
account-rotationunder the local application data directory (~/.local/shareunlessXDG_DATA_HOMEis set) toclaude-code-account-rotationbeside it.
Parked profiles under ~/.claude-profiles and Claude Code's own ~/.claude are not affected.
This is how you move from one tagged release to the next. It is separate from the rename above.
Releases from v1.0.0 on read the files the previous release wrote. There is no migrator and no
schema bump. Do not edit config.json, roster.json, or state/live-owner.json, and do not
copy a template over an existing config.
The app calls the GitHub API only through gh, for Check roles, CI secret writes, kill-switch
toggles, and the secret metadata it reads on those clicks, and never
to look for releases; the dashboard has no "update available" line. Compare
claude-code-account-rotation --version with
https://github.com/melodic-software/claude-code-account-rotation/releases/latest.
Install the two sides in either order. Neither install has to wait for the other, and neither has to stop the other side.
- Download
claude-code-account-rotation-win-x64.exe,claude-code-account-rotation-linux-x64,claude-code-account-rotation-follower-log, andSHA256SUMS. Check each binary and the wrapper against the digest that file lists for it. Replace nothing until they match. Placeclaude-code-account-rotation-follower-login the same directory as the Linux binary. - Windows side. Stop the leader with
POST /api/shutdown, replace the.exe, and start the leader. SendAuthorization: Bearerwith the token on the second line of the leader'sinstance.url, and the sameX-Claude-Code-Account-Rotation: 1header as the other dashboard writes. A 409 means a switch or import is in flight, so the leader was left running: try again once it finishes. If the logon task from melodic-software/provisioning#552 is installed, disable it before the stop and enable it after the start. This repository does not name that task. - WSL side. Replace the Linux binary and the wrapper while the follower runs. Rename a new file over each one, as chezmoi does. Do not copy over the running binary: Linux refuses that with "Text file busy". Do not stop the follower.
What happens after each order:
- Windows first. The new leader finds the follower on the old version. It stops the follower
with that side's own
/api/shutdownand starts it again. The Linux binary installed at that moment is still the old one, so the old build starts again, but now it knows the new leader's version. When the WSL install lands, the follower sees the new build is the leader's version. Within about 15 seconds, once no import is in flight, it stops with exit code 75, and the leader starts the new build. Hand-offs are refused between the two installs, because the versions differ. - WSL first. The follower keeps running the old build, which still matches the old leader, so hand-offs keep working. It knows the leader runs the old version, so it does not stop for the new build. When the new leader starts, it finds the follower on the old version, stops it, and starts the new build.
Either way, a side is stopped only through its own /api/shutdown, which refuses while a switch
or import is in flight. A refused stop leaves that side running on its old version, and the
page shows it as incompatible. Start WSL side, or POST /api/sides/wsl/start on the leader,
tries again: it stops and starts a side on another version, and starts one that is not running.
The leader runs one of these passes at a time per side, so its startup, the button, and a
follower's exit cannot overlap. Only exit code 75 makes the leader start the side again. A
crash, or a stop you sent, is left for you.
Upgrading onto this release from an earlier one, Windows first: the old follower does not stop for a new build by itself, so click Start WSL side once after both installs. Installing WSL first needs no click.
When both sides are done, claude-code-account-rotation --version prints the same version on
each, and the page shows the WSL side online.
There is no --uninstall switch, no uninstall script, and no uninstall route. Remove the tool by
hand, in this order.
- If the logon task from melodic-software/provisioning#552 is installed, disable it. This repository does not name that task.
- Start the leader and the follower if either is not running. Startup finishes or unwinds an open switch or import journal, and it restores a recovery file. Do this even when you already stopped the processes: a journal left behind is what the next start uses to finish a move, and deleting it first leaves a parked pair with nothing to put it back.
- While they are running, the page must show no blocking banner, the same rule as an upgrade. No switch or import may be in flight.
- On each side, look in
recovery/. A*.credentials.jsonoutsidestale/can be the only working lineage: the token endpoint has already invalidated the refresh token in the profile folder. Restart that side, or refresh the card, so the file is restored. Do not continue while such a file remains, even when no card sayscredentials stranded in recovery. - Stop the leader and the follower with
POST /api/shutdown. SendAuthorization: Bearerwith the token on the second line of that side'sinstance.url, and the sameX-Claude-Code-Account-Rotation: 1header as the other dashboard writes. A 409 means a switch or import is still in flight, so the process was left running. - Read
appDataDirectory,profilesRoot,liveConfigDirectory, andstateFilePathfromconfig.json. Read the leader's file, and the follower's whenpeers[].launch.configPathnames one, and take each side'sappDataDirectoryfrom its own file. - Delete each side's app data directory. Also delete
config.jsonwhen--configorpeers[].launch.configPathput that file outside the app data directory. - Delete the executables you installed.
config.json sits in the app data directory unless --config pointed somewhere else. That
directory is %LOCALAPPDATA%\claude-code-account-rotation on Windows, and
claude-code-account-rotation under the local application data directory (~/.local/share
unless XDG_DATA_HOME is set) on Linux and macOS. Those four keys name the directories: delete
each appDataDirectory, and leave profilesRoot, liveConfigDirectory, and stateFilePath.
A file that moved them is the authority, not these defaults.
Leave the profiles root. That tree holds the parked pairs, holder.json, superseded.json, and
.transit. Leave the live directory and Claude Code's state file. Parked profiles under
~/.claude-profiles and Claude Code's own ~/.claude are the defaults those keys name.
What disappears with the app data directory: the roster alias, notes, pause, and browser mapping; the quota cache; the computed queue; and quarantined families.
What survives: the live Claude Code login, and the parked pairs.
Remove on a card is the explicit per-account logout. Uninstall never calls it, and never runs
claude auth logout against the live directory.
After a later install, parked folders show as not on the roster, and Adopt is offered only for the live account.
MIT, see LICENSE.