npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claude-multiacc

v2.0.32

Published

Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.

Readme

claude-multiacc

Multi-account addon for Claude Code AND OpenAI Codex CLI: every claude / claude -p invocation — and every codex / codex exec invocation — runs under a randomly picked subscription account with limit headroom (no API keys, ever). The account sets mirror automatically from the Mac (source of truth) to the deploy server.

Installs as PATH shims + helper CLIs from this repo. Never touches the Claude Code or Codex apps: no binary edits, no install-dir writes, survives claude update / codex update — the real binaries are resolved dynamically at exec time.

One install covers both: the same install.sh / npm i -g claude-multiacc wires up the claude shim + claude-accounts CLI (pool at ~/.claude-accounts) and the codex shim + codex-accounts CLI (pool at ~/.codex-accounts). The two pools are completely independent — separate manifests, credentials, telemetry, and logs — so either provider can be used, re-authenticated, or emptied without touching the other.

Tested on: macOS (bash 3.2, zsh, Claude Code 2.1.207+, Codex CLI 0.147) and Ubuntu 24.04 (bash 5.2). The compatibility suite exercises both providers with no network/quota use; the unified selector and packed npm commands have their own contract suites.

Unified selector for app-robot

DEFAULT_ENGINE=both uses one pure selector across the Claude and Codex pools. It does not change either interactive shim and never launches an agent itself:

multiacc-select --request-json - --response-json -
# equivalent npm dispatcher:
claude-multiacc select --request-json - --response-json -

The command reads one strict UTF-8 JSON request and emits one JSON response using the versioned claude-multiacc/pool-selection.v2 protocol. It ranks the caller's locked candidate snapshot with the same two cuts as the shims — a session-headroom gate, then a band on weekly headroom — and returns one concrete provider/account plus canonical snapshot and selection digests. both is a policy, never an execution engine. The caller remains responsible for durable reservations, database locks, retries, and launching the selected CLI. Successful responses also carry eligible_count and eligible_alternative_count, so callers can persist proof that sole-account reviewer fallback was unavoidable.

Run multiacc-select --version for the selector protocol version (currently 2.0.1). The npm package version is independent and remains available through claude-multiacc --version. See the complete selector contract for the request fields, ranking rules, stable errors, and caller boundary.


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
    (or the login lives in the macOS Keychain — see below)
  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):

  1. CLAUDE_CONFIG_DIR or CLAUDE_CODE_OAUTH_TOKEN already set → pass straight through.
  2. CLAUDE_ACCOUNT=acct-NN → use that account (even if limit-marked — explicit wins).
  3. Drop every account that cannot serve a request — a dead login (expired refresh token, revoked grant, a run that failed to authenticate) or an org-blocked one (Claude Code disabled for that account). Those can only fail, so they never enter selection.
  4. Among the accounts that remain valid on this machine (an OAuth login — .credentials.json, or a macOS Keychain item this session can open — or a server.token that passes its first inference preflight) and not limit-excluded, rank in two cuts (see below). Session gate first: keep only the accounts whose 5-hour bucket is known-fresh and at most 50% used — an account about to be rejected on its session does not deserve a launch whatever its weekly headroom. If nobody clears the gate it steps aside and every candidate stays in: the gate compares, it never empties the pool. Then the weekly band: among the accounts that cleared the gate, form a 30-percentage-point weekly-headroom band around the one with the most remaining weekly headroom. The pool rotates away from the account it just handed out and samples the other accounts in that band at random. This prevents one nominally emptiest account from taking every launch while near-peers idle, while accounts more than 30 points behind remain protected. CLAUDE_MULTIACC_SESSION_GATE=100 turns the gate off (default 50); CLAUDE_MULTIACC_HEADROOM_BAND=0 restores strict best-weekly selection (default 30); CLAUDE_SHIM_SELECT=random uses the whole eligible pool. Every pick records both cuts in selection.log: band=/band-count= (the band width and how many peers it held) and session-gate=/session-ok= (the gate and how many candidates cleared it — session-ok=0 means nobody did and the gate stepped aside; in CLAUDE_SHIM_SELECT=random mode the line reads session-gate=off, because no cut was made).
  5. If every account is limit-excluded → the still-serving limited accounts (a bucket at 90–99%, not an exhausted one) go through the same two cuts — session gate, then strict best-weekly — and one is handed out anyway, with an all-limited fallback= line in selection.log (degraded beats down: the 100% rule).
  6. If nothing is usable at all → stock passthrough, with the reason in selection.log (and, on a terminal, one line naming the accounts to fix).

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). The two buckets therefore answer two different questions, and the picker asks them in that order (operator's decision, 2026-09-03: "among accounts where high session limits it must choose randomly from ones where highest weekly limits").

  • Session is a gate, not a tiebreaker. A candidate clears the gate when its 5-hour usage is known (fresh telemetry) and at most CLAUDE_MULTIACC_SESSION_GATE points spent — default 50, 100 disables the gate. Unknown session usage never clears it. An account that is 85% through its session is minutes from a rejection, so it waits while anyone fresher is available; if nobody clears the gate, everybody is ranked.
  • Weekly headroom picks the winner. Among the accounts that cleared the gate, the picker ranks on weekly headroom (the peak of the durable buckets) and treats everything within CLAUDE_MULTIACC_HEADROOM_BAND points of the leader — default 30, 0 is strict best-weekly-only — as a peer, spreading launches over that band at random. Session usage is not a tiebreaker inside the band: it acts only through the gate. So an account at 10% weekly stays protected from one at 70% weekly, but 10% weekly with an 85%-spent session loses to 70% weekly with a 20% one, because the first never reaches the band.

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.

Unusable accounts are excluded, not rotated into. An account that cannot serve a request is dropped from selection before anything else is considered — it is not eligible as the all-limited fallback either, because a limit-marked account is degraded while this one is down, and handing work to it guarantees a hard failure. Two kinds:

  • Dead login (EXPIRED) — refresh token expired, grant revoked, or a real call that came back "OAuth session expired and could not be refreshed". Fixed by claude-accounts relogin.
  • Rejected setup-token (EXPIRED) — token presence and claude auth status do not prove inference works. Before a portable token carries its first real command on a machine, the shim privately verifies it. A 401 parks that exact account and reselects, so direct, TUI, and --resume calls do not expose the rejected token's failure.
  • Org-blocked (BLOCKED) — the account authenticates fine, but the run comes back "Your organization has disabled Claude subscription access for Claude Code". Handled like any other dead login (claude-accounts relogin re-issues the grant and normally clears it); if it comes back BLOCKED after a re-login, an admin has to enable Claude Code for that account. Telemetry cannot see this — those accounts' usage endpoint answers normally — so only a real call (verify, or a -p run) detects it, and the park deliberately survives credential refreshes (a new access token says nothing about an org policy).

claude-accounts expired lists what is excluded and also labels a token-only account UNVERIFIED until claude-accounts verify or the shim's first-use preflight proves it. Exclusion self-heals, but only against the evidence it was based on: a credential park (dead login) clears as soon as a newer credential lands — a re-login, or a refresh by another process — or when a usage fetch authenticates; a policy park (org block) ignores both, because a fresh access token says nothing about whether the org re-enabled Claude Code, and only a passing real call or a re-login lifts it. Parks the shim writes from a single failed run additionally carry a soft_until stamp and expire on their own — the shim's guess never outlives the evidence for it — while parks claude-accounts writes from proof (an invalid_grant refresh response, a failed real call) stay until the account works again. If every account is unusable the shim still fails open into stock claude, and logs why to selection.log.

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 15 min via launchd on the Mac, cron on the server, plus an opportunistic non-blocking kick from the shim when data is

10 min stale) reads each account's own OAuth usage endpoint — the same data /usage shows. Every bucket the endpoint returns is tracked separately: session, weekly_all, and per-model scoped buckets — weekly_scoped:Fable is its own bucket. Any bucket ≥ 90% ⇒ the account gets a .limited marker (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.

A 0% bucket with no reset window is no data, not an empty account. Real usage always carries the window it resets in, so percent: 0 with a null resets_at is the endpoint declining to answer — and taking it at face value put two weekly-exhausted accounts at the top of the ranking for 31 of ~60 picks on 2026-09-04. Only informative buckets (percent above 0, or a parseable reset time) are aggregated, per signal: weekly_percent is the peak over informative weekly buckets, session_percent over informative session ones, and each is simply omitted when its own group has nothing informative in it — a weekly figure is never borrowed from the session bucket. An account with no informative bucket at all gets a limits.json with none of max_percent/weekly_percent/session_percent, marked "no_data": true. Either way the account ranks unknown — selection needs BOTH readings, so a half-filled document is as unknown as an empty one: never the band leader, never past the session gate, never able to clear a marker, and a pool made only of them logs ranking=BLIND instead of quietly tying every account. Accounts that really are idle are untouched: 0% with a real reset window is informative and still ranks as empty, and one blank per-model bucket beside real ones leaves the real ones in charge.

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 4 min and backs off on every non-2xx, not just a 429 — honoring Retry-After, and parking for 6 h on a refusal the server marks x-should-retry: false. limits --force overrides all of it. The cadence is deliberately unhurried: several machines polling the same accounts every minute earns a 429 with Retry-After: 3600, and telemetry then goes stale for an hour at a time — which is exactly when every account starts scoring unknown and the picker loses its ability to tell them apart.

The usage endpoint needs an OAuth login, not a setup token. A portable server.token (sk-ant-oat01-…) authenticates inference forever, but the usage endpoint refuses it with 403 — OAuth token does not meet scope requirement user:profile: setup tokens are minted without that scope. So an account whose .credentials.json grant has lapsed keeps working perfectly while going permanently dark for telemetry, and a pool where that happens to every account ranks everything unknown and picks at random. claude-accounts status says RANKING IS BLIND when the pool is in that state, and the shim prints an hourly warning on a terminal. The fix is a real sign-in on the machine that polls (claude-accounts login <acct-NN>); since limits.json is one of the things sync pushes, only the source machine needs it — the server and peers inherit the telemetry.

Two freshness windows, on purpose. Ranking trusts telemetry for an hour (CLAUDE_MULTIACC_STALE_AFTER, default 3600 — matching the Retry-After: 3600 the endpoint itself hands out, so a healthy pool is not "stale" for 45 minutes of every hour). The ≥90% exclusion keeps the tighter 15-minute window: ranking chooses between working accounts, while the cutoff declares one unusable, and an account reading 89% an hour ago may be well past 90% now. When nothing is in-window at all, a stale weekly reading is still used if its bucket has not reset yet (a weekly bucket only rises until then, so the number remains a true lower bound) — logged as ranking=DEGRADED. Only when even that is unavailable does selection rank the account last, logged as ranking=BLIND.

If an account's OAuth access token has been expired for a while (idle account, nothing ran claude under it for hours), the refresher renews it directly via the OAuth refresh-token grant — the same endpoint and public client id Claude Code itself uses — and atomically persists the rotated credential back to wherever it came from: that account's .credentials.json (0600), or its macOS Keychain item.

Where a claude OAuth login actually lives (macOS). Claude Code writes each config dir's login to the login Keychain (service Claude Code-credentials-<sha256(dir)[:8]>) from any session that can open it — a Terminal window, a launchd agent — and then deletes .credentials.json. Sessions that cannot open the keychain (ssh, tmux from ssh, background jobs) fall back to the plaintext file, and they also cannot READ a Keychain-held login: those accounts show KEYCHAIN LOCKED there and are excluded from that session's selection, while working normally from the Mac's own session. So a login made over ssh migrates into the Keychain the first time a GUI-session process refreshes its token — the pool reads both places (lib/keychain.py), and an account that must work from everywhere should carry a portable token (claude-accounts mint). Override: CLAUDE_MULTIACC_KEYCHAIN=0 disables the lookup. This is what keeps idle accounts' telemetry fresh so they win selection over busy accounts; without it, stale telemetry ranks neutral and a truly-idle account would lose to a busy-but-fresh one. Refresh failures fail open and back off via <acct>/.oauth-refresh.json (10 min transient, 6 h when the grant looks revoked — the log then says re-login is needed). Overrides: CLAUDE_MULTIACC_TOKEN_URL, CLAUDE_MULTIACC_CLIENT_ID (used by the sandboxed tests; defaults are correct for real use).

Limits the client reports itself. The usage endpoint is not the only source of truth, and it is the one that fails exactly when it matters. Claude Code records every rejection in its session transcript:

{"error":"rate_limit","apiErrorStatus":429,
 "quotaLimits":{"status":"rejected","resetsAt":1787333400,"rateLimitType":"five_hour"}}

That record is free, offline, instant, and carries the real reset time the API handed out — so the shim reads it. This is what makes an interactive session take its own account out of the pool: auto-retry below only ever sees -p runs, so before this a 5-hour limit hit in a tmux session left no trace at all, and the next claude could walk straight back into the same dead account.

Transcripts are not account-scoped (<acct>/projects is a shared symlink by design), so the session → account mapping comes from <acct>/sessions/<pid>.json, which the client maintains only while a run is alive. The shim spawns a short detached poll before exec (the pid survives exec, so it knows the filename) that records the session id — with the session's own start time — in <acct>/.sessions-index; selection then tail-scans those transcripts.

Everything about the rule is built to under-claim rather than over-claim, because a wrongly parked account is worse than a missed limit:

  • One owner per session. claude --continue resumes the same session id under whichever account the pool hands out next (only --fork-session mints a new one), and the transcript is shared. Claiming an id releases it from every other account — and if two accounts end up holding the same id anyway (a crossed race, corrupt state), the ambiguity means neither of them answers for it.
  • Only from the handover. A rejection recorded before this account took the session over stays with the account that earned it; a record that cannot be dated is not attributed at all.
  • Only a private session tree. If <acct>/sessions — or the account dir itself, or a directory nested inside it — is a symlink into somewhere shared, the runs in it prove nothing about who spent the quota, so the scan stays off for that account entirely.
  • A rejection whose resetsAt has already passed is history and excludes nothing; a hostile or unreadable index entry is skipped; CLAUDE_MULTIACC_CLIENT_LIMITS=0 turns the whole scan off.

The resulting marker is tagged reason=client-rate-limit and outlives a clean telemetry pass while its window is still open — first-hand evidence beats a usage payload that disagrees. A weekly rejection is absolute: nothing — neither a shim invocation nor the scheduled limits pass — retires it before its reset epoch, since a weekly bucket cannot fall from a server-proven 100% back under the threshold inside that window. Claude names that bucket from the API's own type (bucket=client:seven_day); codex has no such name in its rollout files, only primary/secondary keys whose meaning moves, so it labels the marker from the window itself — window_minutes >= 1440client:7d, a shorter window ⇒ client:5h, and a window with no window_minutes at all stays client:<key> and remains clearable (fail open, bounded by its own reset). A five-hour marker can still be retired by telemetry fetched well after it (CLAUDE_MULTIACC_CLIENT_LIMIT_CONFIRM_DELAY, 300s), which is what keeps a fresh account from being stranded by an old session limit — before 2026-09-04 that carve-out also let fake-zero telemetry retire weekly markers, re-admitting an exhausted account six times a day. Cost is bounded: the three newest sessions per account, a tail read rather than a whole transcript, and a clean result memoized for 20s (CLAUDE_MULTIACC_CLIENT_SCAN_TTL), which on a three-account pool of large live transcripts is ~2 ms per run amortized.

The codex shim does the same from rollout files ($CODEX_HOME/sessions/<Y>/<M>/<D>/rollout-*.jsonl carry "rate_limits":{"primary":{"used_percent":…,"resets_at":…}}, read newest-first), under the same ownership rule. Note that the installed layout deliberately shares that tree (<acct>/sessions -> ~/.codex/sessions, so codex resume finds every session), so on a default codex pool this scan stays off and the usage endpoint remains codex's only limit signal. Claude accounts keep a private sessions/ registry, so it is fully active there.

A shared tree also gets one shared rollout index. codex refuses to start until its state_<schema>.sqlite has indexed every rollout under $CODEX_HOME/sessions (each file read whole), so a private index per account meant one full scan of the shared tree per account — 10 GB × 12 on a busy Mac — and any launch that met another process's unfinished scan died after 30 s with "timed out waiting for state db backfill … (status: running)", surfaced as the CLI's "local database appears to be damaged". A scan cut short (a probe with a timeout, Ctrl-C) also left its 15-minute worker lease behind, keeping the account unstartable long after its killer was gone. Since 2.0.27 <acct>/state_N.sqlite is a symlink to ~/.codex/state_N.sqlite, exactly like the tree it describes: SQLite resolves the link before naming its -wal/-shm companions, so every process shares one lock set — the same multi-process mode a single CODEX_HOME already runs in. The shim applies this at every launch, and the index name the installed binary carries is linked ahead of time, so a codex schema bump still costs one backfill per machine rather than one per account.

An account with no index of its own — every newly added one, and the case the change exists for — is simply linked. Nothing is renamed and nothing about the shared index's state can block it. An account that already has one is only ever moved while nothing can open it (no -wal and no -shm beside it). codex holds the state database through an sqlx pool whose connections are opened lazily and by path (max_connections(5), create_if_missing(true)), so renaming an index under a live holder would leave its first connection on the old inode while every later one follows the new link — one process, two databases. A codex killed mid-write leaves the pair behind too, and the next clean session on that account removes it, so an account excluded this way rejoins on its own. Once it is closed, the account's index is either retired beside the link as *.private (never deleted, never overwritten — a second retirement gets its own suffix) when the home already has one, or promoted into the home when it does not. The promote goes through link(2), which refuses an existing target: two shims racing to be the first to promote could otherwise rename one's fresh symlink onto the file the other had just promoted, leaving the shared index pointing at itself — ELOOP, every account on the machine unable to start, and repaired by neither codex nor the shim.

Two things stop the sharing entirely. An account whose sessions/ is a real directory keeps a private index — the rule is structural, not a heuristic. And codex's own corruption recovery has the last word: when it judges a database damaged it renames it (with its -wal/-shm) into <CODEX_HOME>/db-backups/sqlite-<ts>-<n>/ and rebuilds, which under a link renames the link. A link sitting in that folder pointing at the shared file is codex's verdict on that file, so the account keeps the index codex rebuilt for it and that name is left alone — handing the link back would hand the damage back.

Auto-retry (-p/--print only, default on, CLAUDE_SHIM_RETRY=0 disables): on an auth- or rate-limit-looking failure the shim retries once on a different account and marks the failed one — a 10-minute cooldown for a rate limit (it heals on its own), or an .expired park for an authentication failure / org block (they do not). The park patterns are deliberately narrow and carry a soft_until stamp, because this grep also sees the model's own answer: a -p run that merely mentions a 403 must never cost an account, and if one slips through it returns to the pool by itself. 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.

Codex support (OpenAI Codex CLI)

Both pools use the same selection policy. See Codex account commands and behavior.

Install / update / uninstall

npm install -g claude-multiacc@latest
codex-accounts verify

npm installs claude-multiacc, claude-accounts, codex-accounts, and multiacc-select into its global bin directory, so account commands work in the current shell. Open a new shell for the claude and codex shims. If auto-setup was skipped, run claude-multiacc install to initialize the pools and shell setup.

See installation, updates, removal, and command-not-found recovery for npm, npx, git checkout, and server instructions.

Account lifecycle, synchronization, fleet integration, environment switches, verification, troubleshooting, and security guidance continue in Account and fleet operations.