claude-multiacc
v1.0.4
Published
Multi-account addon for Claude Code: every claude / claude -p runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.
Maintainers
Readme
claude-multiacc
Multi-account addon for Claude Code: every claude / claude -p invocation runs under a
randomly picked subscription account with limit headroom (no API keys, ever). The
account set mirrors automatically from the Mac (source of truth) to the deploy server.
Installs as a PATH shim + helper CLI from this repo. Never touches the Claude Code app:
no binary edits, no install-dir writes, survives claude update — the real binary is
resolved dynamically at exec time.
Tested on: macOS (bash 3.2, zsh, Claude Code 2.1.207) and Ubuntu 24.04
(bash 5.2, Claude Code 2.1.201). Test suite: tests/run-tests.sh (35 sandboxed tests,
no network / no quota — passes on both platforms).
How it works
MAC (source of truth) SERVER (mirror)
~/.claude-accounts/ /root/.claude-accounts/
accounts.json <- manifest accounts.json (synced)
acct-01 -> ~/.claude ("adopted") acct-01/server.token (synced, 600, once minted)
acct-02/ (registered) acct-02 -> /root/.claude ("adopted")
server.token / .credentials.json
selection.log sync.log health.log /usr/local/bin/claude -> repo shim
repo bin/ first on PATH (rc-file block) /root/claude-multiacc/ (addon repo)Shim selection order (identical file on both machines, bin/claude):
CLAUDE_CONFIG_DIRorCLAUDE_CODE_OAUTH_TOKENalready set → pass straight through.CLAUDE_ACCOUNT=acct-NN→ use that account (even if limit-marked — explicit wins).- Among accounts valid on this machine (
.credentials.jsonorserver.tokenpresent) and not excluded, pick the one with the most remaining headroom (see below). Ties break randomly to spread load.CLAUDE_SHIM_SELECT=randomrestores plain uniform-random selection. - If every account is excluded → the highest-headroom valid account anyway + a warning
in
selection.log(degraded beats down: the 100% rule).
Which account has "the most headroom"? A Max subscription has three limits that a
Fable request consumes at once: the 5-hour session bucket, the weekly all-models
bucket, and the weekly Fable bucket. Anthropic's docs confirm these reset on very
different horizons — the session bucket refills every ~5 hours, but weekly buckets only
refill on the account's fixed weekly reset (days away). So the picker ranks primarily on
weekly headroom (the peak of the durable buckets) and uses the self-healing session
bucket only as a tiebreaker. An account sitting at 85% session / 10% weekly outranks one
at 20% session / 70% weekly: the first only needs a few hours to recover, the second is
burning headroom that's gone until next week. (score = weekly% × 1000 + session%,
lowest wins.) Exclusion still fires on any bucket ≥ 90% — a full session bucket
really does block right now — but that marker expires when the session resets, not days
later.
The shim prints nothing, logs timestamp account cwd (never prompt text) to
selection.log, and execs the real binary — stdin/stdout/exit codes pass through
byte-identically. If anything is missing (no manifest, no accounts, unreadable state,
even an unset HOME) it fails open into plain passthrough.
Limit-aware marking. claude-accounts limits (every 60s via launchd on the Mac,
cron on the server, plus an opportunistic non-blocking kick from the shim when data is
3 min stale) reads each account's own OAuth usage endpoint — the same data
/usageshows. Every bucket the endpoint returns is tracked separately:session,weekly_all, and per-model scoped buckets —weekly_scoped:Fableis its own bucket. Any bucket ≥ 90% ⇒ the account gets a.limitedmarker (with the bucket's reset time) and leaves the random pool until that bucket resets or a refresh shows it back under threshold.
Bucket parsing is shape-agnostic: buckets are derived from whatever the endpoint
returns (kind, plus scope.model when present), so if Anthropic renames a bucket,
reshapes the payload, or drops the per-model Fable separation entirely, the remaining
buckets keep being tracked and the ≥90% rule keeps working. Unparseable entries are
skipped, an all-legacy payload falls back to five_hour/seven_day, and a payload the
code cannot read at all degrades that one account (fail open), never the run.
Telemetry failures never block work: no fresh data ⇒ account treated as available. The
endpoint rate-limits per account, so the refresher skips accounts fetched in the last 45s
and backs off exponentially (honoring Retry-After) on a 429 — limits --force overrides
both. If an account's OAuth access token has expired (quiet machine, nothing ran claude for
hours), the refresher first runs a zero-cost claude auth status under that account to let
the app refresh its own credentials, then fetches; if that fails it logs and fails open.
Auto-retry (-p/--print only, default on, CLAUDE_SHIM_RETRY=0 disables): on an
auth/rate-limit-looking failure the shim marks the account with a 10-minute error
cooldown and retries once on a different account. Output is buffered so a retried call
never double-emits. Only engages when stdin is finite (tty / regular file / /dev/null)
and ≥2 accounts are eligible; service-spawned pipes take the plain exec path untouched.
Install / update / uninstall
npm (recommended)
npm install -g claude-multiacc # postinstall wires up the shim, CLI, and scheduled jobs
# or run it once without installing globally:
npx claude-multiaccclaude-multiacc then wraps everything:
claude-multiacc # install or update the addon (idempotent)
claude-multiacc self-update # npm i -g @latest + re-install
claude-multiacc uninstall [--purge-data]
claude-multiacc list | status | add <email> | ... # passthrough to claude-accountsAutomatic updates: a globally-installed package re-runs install.sh from its own
postinstall, so npm i -g claude-multiacc@latest fully updates the shim, CLI, and jobs.
A weekly job (launchd on macOS, cron on Linux) runs claude-accounts self-update to pull
the latest npm release on its own; set CLAUDE_MULTIACC_AUTOUPDATE=0 before install to
opt out. update-notifier also prints a one-line nudge when a newer version is on npm.
(Note: newer npm versions gate install scripts; if postinstall is blocked, just run
claude-multiacc install once.)
New versions publish to npm automatically from main via GitHub Actions
(.github/workflows/publish.yml, OIDC Trusted Publishing — no long-lived token after the
first bootstrap publish; scripts/auto-version.mjs bumps the patch above the last release).
From a git checkout
git clone <repo> && cd claude-multiacc && ./install.sh # macOS or Linux, idempotent
./install.sh --server [email protected] # override sync target
git pull && ./install.sh # update (data untouched)
./install.sh --uninstall # restores stock behavior, keeps account data
./install.sh --uninstall --purge-data # also deletes ~/.claude-accountsself-update auto-detects which of these you used (npm global vs git checkout).
What install does (all reversible, nothing else):
- macOS: marked PATH block at the END of
~/.zshenv,~/.zprofile,~/.zshrc(+ bash rc files if present) — end-of-file placement matters because those files re-prepend~/.local/bin; launchd agentscom.claude-multiacc.limits(60s) and.health(weekly Mon 09:17). Warns if this Mac stores credentials in the Keychain (file-based credentials are required for per-dir logins). - Linux (root): PATH block in
~/.bashrc+/etc/profile.d/claude-multiacc.sh, shim symlink at/usr/local/bin/claude(shadows via PATH order — on the systemd default PATH too; the original binary is untouched), cron entries for limits/health. - Both:
~/.claude-accounts/skeleton +accounts.jsonmanifest.
Accounts
claude-accounts list # pool at a glance
claude-accounts status # per-account auth health, per-bucket limits incl. Fable, markers, last pickEvery account in the pool is authenticated — add cannot register one otherwise.
Adopt this machine's existing login (no browser needed)
claude-accounts import [email protected] --id acct-01 --home mac
claude-accounts adopt acct-01adopt makes the account dir a symlink to ~/.claude — the machine's existing
login joins the pool with a single credential file (no copy of the OAuth grant exists
anywhere else, so refreshes can never race). This is how the two current accounts run:
acct-01 = the Mac's login, acct-02 = the server's login (adopted server-side).
Add a fresh account (~1 min, one browser sign-in)
claude-accounts add # no email needed — read back from the sign-in
# or name it explicitly (fast-fails on a duplicate, and pre-fills the login page):
claude-accounts add [email protected]This runs the normal full Claude Code login — the exact same
claude auth login flow the app itself uses (claude.com/cai/oauth/authorize, full
Claude Code scopes). A browser opens (or a sign-in link is printed); sign in to the
account you're adding, approve, and if a code is shown, paste it back. It draws on the
subscription — never API keys.
Using the full login on purpose: it does not hit the "Sign in again to continue"
step-up wall that the narrow long-lived-token grant triggers, and it stores an
auto-refreshing .credentials.json. That credential is machine-local (never synced),
which is what keeps two machines from invalidating each other's refresh token — so an
account you add on the Mac runs on the Mac, and you add it on the server (over SSH) if
you want it there too. The account is registered only after the signed-in email is read
back; an aborted login leaves nothing behind, and duplicate emails are refused.
claude-accounts login acct-NN # complete/refresh an existing account (full login)
claude-accounts add [email protected] --token # portable setup-token instead: works on BOTH
# machines and syncs to the server, but needs a
# recent sign-in (may show the step-up screen)
claude-accounts login acct-NN --token # same, for an existing accountUse --token when you specifically want one account usable on the server via the Mac's
sync (the ~1-year inference-only token that mirrors over). For everyday adds, the default
full login is smoother. status warns when a token nears end of life; re-run login.
Both accounts currently in the pool are live and verified: acct-01 ([email protected], Mac) and acct-02 ([email protected], server) — each adopted from its machine's existing login, so each runs on the machine that holds its credential.
Remove
claude-accounts remove acct-NN # deletes locally, propagates to the serverServer sync
Mac → server, one-way (Mac = source of truth), fired automatically by
add/import/remove/mint and manually via:
claude-accounts syncPushes the manifest + per-account server.token (0600) + one-time seeds; never
syncs .credentials.json in either direction (two machines refreshing one OAuth grant
invalidate each other). Removals propagate; an empty/corrupt manifest refuses to sync
(guard against wiping the server pool). After every push the server re-seeds dirs and
re-runs its quick verification matrix (post-sync hook). Everything logs to sync.log;
failures are loud and non-zero.
Pinning & env switches
| Env | Effect |
| --- | --- |
| CLAUDE_ACCOUNT=acct-03 | pin this invocation to one account (wins over markers) |
| CLAUDE_CONFIG_DIR=... | shim passes straight through (scripts can pin the old way) |
| CLAUDE_MULTIACC_DISABLE=1 | bypass selection entirely |
| CLAUDE_SHIM_RETRY=0 | disable the -p auto-retry |
| CLAUDE_ACCOUNTS_DIR=... | relocate the pool (used by the test suite) |
Verification
tests/run-tests.sh # 35 sandboxed tests, no quota
claude-accounts verify # real matrix: `claude -p "reply OK"` per authed account
claude-accounts verify --quick# auth presence/expiry only, no inference
claude-accounts limits # live per-bucket usage incl. the Fable bucket
claude-accounts health # limits + full verify, logs to health.log, notifies on failure (Mac)Verified end-to-end on both machines (2026-07-13): full matrix PASS, 20-invocation shim
spread clean, real project -p through the shim OK on Mac (~/pipeline) and server
(/opt/inapp-onboarding), uninstall→stock→reinstall roundtrip byte-identical, three
server systemd services (vpn-server-management, inapp-onboarding, vpn-seo-panel)
repointed via CLAUDE_BIN=/usr/local/bin/claude and restarted healthy.
Troubleshooting
clauderesolves to the real binary, not the shim — open a new shell, or check that the marked block is the LAST PATH manipulation in your rc file (grep -A2 'claude-multiacc >>>' ~/.zshrc).- An account never gets picked —
claude-accounts status: no auth on this machine, or an active.limitedmarker (shows bucket + minutes to reset). - Everything marked limited — the shim still runs (least-utilized fallback);
check
selection.logforall-limitedlines. - Sync fails —
tail ~/.claude-accounts/sync.log; it's ssh/rsync to the manifest'sserver(BatchMode — needs key auth). - A service bypasses the shim — it spawns an absolute path. Point its env
(
CLAUDE_BINetc.) at/usr/local/bin/claude. Docker-internalclaudeinstalls (e.g. openclaw live-test containers) are out of scope for a host shim. addsign-in says "Sign in again to continue" — that's Claude's step-up authentication, not a tool error: granting a long-lived token needs a recent sign-in. Open a fresh incognito window, sign in to the target account at claude.ai first, then paste the link and approve. The approval scope isuser:inference("Contribute to your Claude subscription usage") — that's correct and minimal.- Limits look stale — launchd:
launchctl list | grep claude-multiacc; cron:crontab -l | grep claude-multiacc. The shim also kicks a refresh when data is >3 min old.
Security notes & known risks
- Tokens and credentials live as 0600 files under
~/.claude-accounts(and/root/.claude-accountson the server). Server compromise = account access. selection.logrecords timestamps/account/cwd only — never prompt text.- Rotating multiple subscriptions to spread usage may be flagged by anti-abuse systems; accounts can be banned for limit circumvention. Known and accepted by the operator.
- The usage endpoint is the internal one
/usageconsumes; if it changes shape, limit tracking fails open (accounts stay available, error-retry is the backstop).
