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.19

Published

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

Downloads

10,632

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 currently covers 645 sandboxed cases with no network/quota use; the unified selector has its own adversarial contract suite.

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 by usable weekly/session 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, pick the one with the most remaining headroom (see below). On equal headroom the pool rotates away from the account it just handed out and samples the rest at random — so quitting a session and starting another one moves you along the pool, while a burst of parallel claude -p runs still spreads (a strict "oldest first" order would send every member of the burst to the same account). CLAUDE_SHIM_SELECT=random restores plain uniform-random selection.
  5. If every account is limit-excluded → the highest-headroom authenticable account anyway + a warning 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). 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.

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.

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. 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.

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)

The exact same machinery, instantiated a second time for Codex. Everything documented above about the claude pool holds for the codex pool with these translations:

| Claude side | Codex side | | --- | --- | | bin/claude shim, claude-accounts CLI | bin/codex shim, codex-accounts CLI | | pool ~/.claude-accounts | pool ~/.codex-accounts | | CLAUDE_CONFIG_DIR per-account dirs | CODEX_HOME per-account dirs | | .credentials.json / macOS Keychain item (OAuth, machine-local) | auth.json (ChatGPT OAuth, machine-local) | | claude -p auto-retry | codex exec auto-retry | | Anthropic OAuth usage and Fable buckets | Codex usage endpoint and per-model buckets | | CLAUDE_* pool controls | equivalent CODEX_* controls |

Same selection rule (weekly headroom primary, session tiebreak, ≥90% any-bucket exclusion, equal scores rotate), same marker semantics (.limited cooldowns, .expired parks with credential/policy scoping and soft expiry), same fail-open guarantees, same sync safety guards. The codex shim engages the buffered auto-retry only for codex exec runs with finite stdin, exactly like -p on the claude side.

Codex-specific notes:

  • Auth is the Codex device-code sign-in by default: codex-accounts add prints a URL + one-time code you can open in ANY browser (this machine, your laptop, a phone), so it works identically on a local Mac, over SSH, and on servers — there is no portable setup-token equivalent for Codex, so a server account is signed in ON the server the same way. --browser opts into the localhost browser-callback flow instead (only works when the browser runs on the same machine — the callback goes to localhost:1455). Either way the login runs with CODEX_HOME pointed at the account dir, and identity is verified offline from the id-token before anything is registered.
  • auth.json is never synced in either direction, for the same reason .credentials.json never is: the refresh token rotates, and two machines refreshing one grant strand each other. Sync pushes manifest + config.toml seeds + advisory limit state only.
  • Windows are classified by length, not by name: any window ≤6h counts as the self-healing session signal, anything longer as durable/weekly. A hard limit_reached/allowed:false verdict marks the account even if no window shows ≥90%, and a reshaped payload is still scanned recursively for window-shaped objects (fail open if nothing parses).
  • Idle-account telemetry stays fresh the same way: a long-expired access token is renewed via the OAuth refresh-token grant (auth.openai.com/oauth/token, the CLI's own public client id) and the rotated credential is persisted 0600. Overrides for tests: CODEX_MULTIACC_TOKEN_URL, CODEX_MULTIACC_CLIENT_ID, CODEX_MULTIACC_USAGE_URL.
  • Earned usage resets are redeemed automatically at 95% used (5% remaining) or when the backend reports the limit finished. The limits refresher checks the reset credits belonging to that same account, uses the soonest-expiring available credit, and writes a per-account/window idempotency key before redemption so a lost response or simultaneous fleet poll cannot spend a second reset. Set CODEX_MULTIACC_AUTO_RESET=0 only for emergency rollback/testing.
  • API-key logins are rejected — ChatGPT subscription accounts only, matching the addon's no-API-keys rule.
codex-accounts add                    # sign in a new ChatGPT account (device code —
                                      # open the URL in any browser, enter the code)
codex-accounts add --browser          # localhost browser-callback flow instead
codex-accounts import <email> --id acct-01 && codex-accounts adopt acct-01
                                      # adopt this machine's existing ~/.codex login
codex-accounts list | status | expired | relogin | verify | limits | health | sync
claude-multiacc codex <cmd>           # same commands via the npm wrapper

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-multiacc

claude-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-accounts

Automatic 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 daily job (launchd on macOS, cron on Linux, 04:07) runs claude-accounts self-update to pull the latest release on its own — npm i -g @latest for npm installs, git pull + reinstall for git checkouts; 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-accounts

self-update auto-detects which of these you used (npm global vs git checkout).

Servers / machines without repo access: install via npm — the registry is public, so the daily self-update needs no credentials at all (no rsync, no keys). A git checkout self-updates too, but only if git pull can authenticate; for a private repo on a server that means a read-only deploy key (the 138 server runs this way: core.sshCommand pinned to its deploy key). A plain copied tree is the one layout that can NOT self-update — don't ship the addon that way.

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 agents com.claude-multiacc.limits + .codex-limits (5m), .health + .codex-health (weekly Mon morning), and .update (daily 04:07). Notes when this Mac keeps Claude Code logins in the Keychain (the pool reads them; ssh sessions cannot — mint portable tokens for accounts that must work from everywhere).
  • Linux (root): PATH block in ~/.bashrc + /etc/profile.d/claude-multiacc.sh, shim symlinks at /usr/local/bin/claude and /usr/local/bin/codex (shadow via PATH order — on the systemd default PATH too; the original binaries are untouched), cron entries for limits/health (both providers) + the daily auto-update.
  • Both: ~/.claude-accounts/ skeleton + accounts.json manifest.

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