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

@dp-pcs/stacked

v0.2.7

Published

Run Claude Code on whichever of your accounts has the most quota left, and roll over when one runs out.

Readme

stacked

Run Claude Code on whichever of your accounts has the most quota left, and roll over automatically when one runs out.

If you have more than one Claude account and keep hitting usage limits, the manual fix is to log out, log in as someone else, and lose your session. stacked does that for you, without the logout.

What it actually does

Claude Code decides which account you are on by reading one environment variable, CLAUDE_CODE_OAUTH_TOKEN. stacked stores a token per account in your macOS Keychain, checks how much quota each has left, and launches Claude Code with the best one set. That is the whole trick — there is no logout, no login, and no browser round trip.

It configures nothing else. Routing, model names, gateway headers — all of that stays in Claude Code's own ~/.claude/settings.json, which stacked reads and never writes. If your org routes you through a gateway, stacked follows it automatically. There is exactly one source of truth and stacked is not it.

Requirements

  • macOS (uses the system Keychain via security)
  • Claude Code, already installed and working
  • Node 18+ (only to install; the tool itself is shell)
  • Two or more Claude accounts you are entitled to use

Install

npm install -g @dp-pcs/stacked

Or without installing:

npx @dp-pcs/stacked setup

Setup

stacked setup

The wizard checks your Claude Code config, then asks for one Keychain entry per account. If an entry already exists it validates it; if not, it offers to store one for you. Prefer to do it by hand, or scripting it for a team? See MANUAL.md.

To get a token for an account: log into that account in Claude Code, then run claude setup-token. Repeat per account.

Each token is checked before it is saved. If a paste or verification fails, choose Retry, Skip, or Cancel without restarting setup. Accounts you have already completed remain registered, and rerunning setup preserves existing accounts. A confirmed quota limit is accepted; an authentication, network, or gateway error leaves the previous token untouched.

To replace the token for just one registered account:

stacked account repair claude_work

The replacement is hidden while you paste it, validated through the account's configured route, and saved only after verification. Its registry entry and model group are preserved.

Name your entries carefully. No API response identifies an individual seat, so stacked cannot tell you which login a token belongs to. The name you choose is the only label you will ever have. claude_personal and claude_work beat claude_1 and claude_2.

Daily use

stacked run                                  # launch on the best account
stacked run --auto-rotate                    # interrupt and resume if it truly exhausts
stacked run --overnight                      # auto-rotate, and wait out a reset
stacked run --dangerously-skip-permissions   # flags pass straight through
stacked run -p "summarise this repo"         # non-interactive too
stacked -p "summarise this repo"             # a leading flag implies `run`

Anything you would type after claude, type after stacked run. There is no separator and no special syntax.

If you always use the same flags, alias it:

alias cs='stacked run --dangerously-skip-permissions'

When the account you are on runs out mid-session, stacked rotates to the next one and relaunches with --continue, re-applying the flags you originally passed. You will see it happen; it is not silent.

By default, stacked will not kill a live Claude Code session. Add --auto-rotate when unattended continuation matters more than preserving the current in-flight response. In that mode, the watcher interrupts Claude Code only after the active account is truly exhausted, then stacked relaunches on the next usable account with --continue. --overnight is the same opt-in behavior with a shorter default watch interval for long unattended runs.

In either unattended mode, running out of every account is no longer the end of the run: stacked waits for the earliest quota window to reset and then carries on. See Waiting for a reset. Ctrl-C stops it.

Keep your model across rotations

Choose the model when you launch. stacked passes --model to Claude Code and re-applies it on every rotation:

stacked run --overnight --model 'opus[1m]'
stacked run --overnight --model 'sonnet[1m]'

Use [1m] only when your model and account route support 1M context. Without it, an unrecognized gateway model name can make Claude Code assume a 200K window and compact there even when the backend supports more. Quote names containing brackets so your shell does not expand them.

For a preference you set once, add an alias to ~/.zshrc:

alias stacked-opus="stacked run --overnight --model 'opus[1m]'"

Run stacked-opus afterward, or use stacked run --model ... for a different choice. stacked has no separate stored model preference or model-selection prompt. This preserves the launch selection; a later /model change inside Claude Code is not captured by stacked and can revert on rotation.

In this version, detecting a gateway group makes stacked set Sonnet as the child's default. An explicit --model takes precedence, so use it even if you already chose a default in Claude settings. The opus and sonnet aliases follow each account's group; an explicit <group>/<model> stays fixed, including its group. See the TrueFoundry example for versioned model names and live validation details.

When an account runs out mid-session

stacked cannot switch accounts while Claude Code is running — auth is read at startup, and killing a live session to force a rotation risks losing unsaved conversation state. So the handoff is:

  1. A desktop notification warns you as the account approaches its limit
  2. You exit Claude Code when it suits you
  3. stacked immediately relaunches on the next account with --continue

While a session runs, a background watcher polls the active account every 5 minutes and notifies twice at most: once at 85% utilization ("wrap up soon"), and once if it runs dry ("exit and stacked will switch accounts and resume"). Notifications rather than terminal output, because Claude Code owns the terminal while it runs.

A failed poll — a network blip, or a poll that was itself rate-limited — is not treated as the account dying; the watcher rides out up to STACKED_WATCH_MAX_FAILURES consecutive failures (default 5) before giving up, so one blip no longer silently ends your quota warnings for the session.

Turn it off with STACKED_WATCH=0; tune with STACKED_WATCH_INTERVAL (seconds) and STACKED_WATCH_WARN (0–1).

For unattended sessions, stacked run --auto-rotate lets the watcher interrupt Claude Code after true exhaustion and relaunch on the next usable account with --continue. stacked run --overnight enables auto-rotation and uses a 60-second watch interval unless STACKED_WATCH_INTERVAL is already set.

Waiting for a reset

Unattended only — --auto-rotate or --overnight. Without it, stacked run still gives up the moment nothing is usable, exactly as before.

A 5-hour window is shorter than a night, so an account that runs dry at 22:00 is usable again around 03:00. Rotating alone cannot exploit that: once every account has been tried the run is over, and stacked would sit dead while all of them quietly refilled. So when nothing is usable, it waits.

Where the earliest opening is takes two steps, and the direction matters in both:

  • Within an account, the latest reset among the windows currently blocked. An account is usable only when every window is allowed, so a seat blocked on both its 5-hour and 7-day windows comes back on the later of the two.
  • Across accounts, the soonest of those.

Then it sleeps until then and resumes the same conversation with --continue, forgetting which accounts it had ruled out — the ones excluded earlier are precisely the ones that just reset.

Not every exhausted account says when it resets: an HTTP 429 carries no headers at all. When nothing gives a time, stacked re-probes every STACKED_WAIT_POLL seconds (default 15m) instead of guessing. A reset epoch that has already passed while the server still says rejected is treated the same way — reset times round to ten-minute buckets and clocks drift, so believing it would mean picking the account, being rejected immediately, and looping.

Bounds, because a hands-off run should not sit forever:

  • STACKED_MAX_WAIT (default 86400, one day) is the total time spent sleeping across the invocation. Time spent actually running Claude Code does not count against it.
  • If the earliest reset is further out than the budget allows — a 7-day window can be days away — it stops immediately and says so, rather than sleeping out the whole day to fail at dawn.
  • Accounts whose token is rejected are never waited on; a revoked token does not come back on a timer. If that is all of them, the run ends at once.
  • STACKED_MAX_WAIT=0 turns waiting off entirely.
  • STACKED_WARM=1 opts in to pre-warming; it is off by default.

Every rotation, wait and resume is timestamped on stderr, and notified, so a redirected log tells the whole story:

stacked: [2026-09-05 02:14:07] acct_c is out of quota — rotating and resuming
stacked: [2026-09-05 02:14:09] every account is out of quota (tried: acct_a,acct_b,acct_c) — waiting for the earliest reset
stacked: [2026-09-05 02:14:11] waiting 2h46m before the next check
stacked: [2026-09-05 05:00:22] acct_a is usable again — resuming

Keeping idle accounts from going stale

The same two modes can also pre-warm, with STACKED_WARM=1. It is off by default: every warm is a real API call, and whether starting a window early helps at all is not yet established (see the caveat below).

A 5-hour window only starts when an account is used, and the in-session watcher polls only the account you are on — so an account left idle through a long session does not begin its next window until stacked finally rotates to it, pushing every later reset further out. The account you reach last ends up being the one still ticking when you would rather it had already reset.

So a single probe fires just after an idle account's window expires, starting the next one immediately. Over a night that means more window boundaries and less waiting.

Three things worth being clear about:

  • It adds no quota. The weekly cap is untouched; this only changes when capacity lands.
  • It cannot start a window early. There is no way to reset one — a request inside a live window does nothing — so the probe fires after expiry, never before.
  • At most one probe per account per window. Each <account, reset-epoch> pair is tried once: if a probe comes back with the same overdue epoch, the window did not start and it is never retried. One account per watcher tick, so several expiring together never burst against the per-IP limit.

The caveat, and why it is off by default. Whether this helps depends on windows anchoring to use rather than advancing on a schedule of their own, and the evidence is not unanimous — an account observed at 0.0% utilization still had a live window. So every warm logs the reset epoch before and after, and warmed <account> lines in an overnight log say either next window now resets in ... or reset did not move. Turn it on for a night, read the log, and decide from your own data.

Two practical notes. A laptop that sleeps cannot wake to meet a reset — it resumes when you open the lid — so wrap long runs in caffeinate:

caffeinate -is stacked run --overnight -p "the task"

And the run exits 75 when the budget is spent, distinct from 1 for a configuration or credentials problem, so a wrapper can retry on the former:

until stacked run --overnight -p "the task"; do
  [ $? -eq 75 ] || break   # anything else is not going to fix itself
  sleep 3600
done

Reading stacked status

ACCOUNT                  HEALTH     5H     7D     RESETS    CRED      ORG
claude_personal          healthy     13%    40%   0h17m     6bf512ff  a8dc2ea4
claude_work              healthy      1%     0%   3h37m     0c5a5eea  7493d817
claude_old               invalid      0%     0%             517f2606

| column | meaning | |---|---| | HEALTH | healthy, near-limit (usable — either past a configured threshold or carrying the server's own allowed_warning), exhausted (out of quota), invalid (token rejected), error (no response) | | 5H / 7D | how much of the 5-hour and 7-day windows you have used | | RESETS | when the 5-hour window rolls over | | CRED | a local fingerprint of the token — see below | | ORG | organization id, informational only |

How it picks an account

Each account is probed with a max_tokens: 1 call. That returns Anthropic's rate-limit headers without meaningfully consuming quota:

anthropic-ratelimit-unified-5h-utilization: 0.13
anthropic-ratelimit-unified-7d-utilization: 0.40
anthropic-ratelimit-unified-5h-status:      allowed

An account counts as exhausted only when Anthropic rejects it:

  • the server reports a window as rejected (or any status outside the allowed family)
  • the call returns HTTP 429

allowed_warning is not a rejection. Once a window passes a soft threshold the server switches its status to allowed_warning, adds anthropic-ratelimit-unified-<window>-surpassed-threshold (typically 0.75), and carries on serving the request. stacked treats that as near-limit, not as an outage — a weekly window at 78% must not retire an account whose 5-hour window is untouched.

If utilization crosses STACKED_THRESHOLD (default 0.95), the account is also near-limit: stacked will prefer another usable account, but it will still use that account if it is the best remaining option. This keeps rotation proactive without turning remaining quota into a false outage.

Among usable accounts, stacked picks the most headroom: 1 - max(5h, 7d).

The probe uses the same base URL and headers as your real traffic, so if you go through a gateway, so does the probe.

Picking an account probes every account back to back, and Anthropic rate-limits /v1/messages per source IP — so with five or more accounts that fan-out starts tripping limits on the probes themselves. To avoid that, probe results are cached under ~/.config/stacked/cache for STACKED_CACHE_TTL seconds (default 300), with the expiry jittered ±25% so a batch written together doesn't all expire together. stacked status marks when it's showing cached figures; set STACKED_CACHE_TTL=0 to always probe live. The decision that actually gates rotation — the check after a session ends — is never cached, and it refreshes the cache on its way out. A probe that fails outright (dropped connection, or a probe that was itself rate-limited) falls back to the last good reading for up to STACKED_CACHE_GRACE seconds rather than blanking the account out of the running.

Telling accounts apart

There is no header that identifies an individual seat. The API exposes only anthropic-organization-id and anthropic-workspace-id, both org-scoped, so several seats in one organization report identical values. stacked therefore shows:

  • CRED — a local sha256 prefix of the token. Always distinct per credential, computed on your machine; nothing is transmitted to produce it.
  • ORG — informational. Useful when your accounts span organizations, meaningless when they do not.

What actually matters is whether two entries share a quota pool, because rotating between two tokens for the same seat gains you nothing. status checks that two ways:

  1. The same token registered twice — caught by comparing CRED fingerprints. Certain, not inferred. This is the mistake people actually make: re-run setup-token, store it under a second name, and now two entries point at one account.
  2. Two different tokens for one seat — inferred from organization plus both reset epochs. Reset windows anchor to an account's first use, so two tokens for one seat report identical resets while separate seats do not. This holds even at zero utilization, so brand-new accounts are covered.

Utilization is deliberately excluded from that second check: it moves between probes, and including it would let two tokens for one seat be sampled either side of a usage tick and score as separate accounts.

Verified against five real accounts — a seat in its own organization, two distinct seats sharing an organization, and two tokens minted for one of those seats. Only the last pair is reported.

Resets appear to round to 10-minute buckets, so two genuinely separate seats first used in the same bucket could collide. That is why the note says probable, not certain.

Working through a gateway

If ~/.claude/settings.json points at a gateway, stacked follows it. Nothing to configure here.

This works because your account token stays the Authorization bearer while the gateway header only authenticates the hop in front of Anthropic. The gateway forwards your credential, so Anthropic returns your seat's rate-limit headers and rotation behaves exactly as it does direct. Every rotated account stays visible in your gateway's telemetry.

It does not apply to shared-key gateways, where the gateway holds its own upstream credential. There everyone draws on one pool, there are no per-user accounts, and there is nothing for stacked to rotate.

Billing safety

Three things silently switch you from subscription usage to metered API billing. stacked doctor checks all of them:

  • a top-level "model" key in settings.json
  • ANTHROPIC_API_KEY set in your environment
  • ANTHROPIC_AUTH_TOKEN set in your environment

stacked run clears the last two for the process it launches, so a rotated session is safe even if your shell is not. stacked supplies the selected setup token through CLAUDE_CODE_OAUTH_TOKEN, not ANTHROPIC_AUTH_TOKEN. Plain claude will still pick up shell values, so fix them at the source.

Claude Code Teams + Fable through a gateway

Claude Code has a known setup-token/OAuth entitlement bug where Team seats can be prompted for usage credits when selecting Fable. stacked exports the subscription entitlement workaround for the Claude Code process it launches, including Fable 5.1.

If your gateway uses prefixed model slugs, add the group as the optional second column in ~/.config/stacked/accounts:

claude_work claude-work-group

or configure it in Claude Code settings by prefixing ANTHROPIC_DEFAULT_HAIKU_MODEL. Do not add ANTHROPIC_AUTH_TOKEN; stacked already passes the selected account token as CLAUDE_CODE_OAUTH_TOKEN.

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://<your-gateway-host>",
    "ANTHROPIC_CUSTOM_HEADERS": "x-<gateway>-api-key: <gateway-token>",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "<group>/claude-haiku-4-5-20251001",
    "ANTHROPIC_DEFAULT_FABLE_MODEL": "<group>/claude-fable-5-1",
    "ANTHROPIC_CUSTOM_MODEL_OPTION": "<group>/claude-fable-5-1",
    "ANTHROPIC_CUSTOM_MODEL_OPTION_NAME": "Fable",
    "ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION": "Fable 5.1",
    "CLAUDE_CODE_SUBSCRIPTION_TYPE": "max",
    "CLAUDE_CODE_RATE_LIMIT_TIER": "default_claude_max_5x"
  }
}

For a TrueFoundry-specific setup, see examples/truefoundry.md.

Troubleshooting

Start with stacked doctor. It reports config, every account, and the billing checks in one pass.

| symptom | cause | |---|---| | invalid in status | credentials or access were rejected. Check gateway authentication if applicable, then use stacked account repair <name> to replace a revoked or mistyped token. | | error in status | no response. Network, or a gateway rejecting the request — try stacked probe <account> for the raw reply. | | no healthy accounts available | every account is exhausted or invalid. status shows when the earliest resets. In --auto-rotate/--overnight this means waiting was off (STACKED_MAX_WAIT=0) or every token is rejected. | | exit 75 | the wait budget ran out, or the earliest reset is beyond it. status shows the resets; raise STACKED_MAX_WAIT, or wait for a 7-day window. | | resumed, then went straight back to waiting | the window reopened but is still effectively full. stacked backs off to STACKED_WAIT_POLL rather than spinning. | | unknown command | you typed a subcommand that does not exist. A leading flag is fine (stacked -p ...); a bare word is not. | | "share a quota pool" note | two entries look like the same seat. If you know they are separate, ignore it. | | unrecognized-model warning / compacts at 200K | Claude Code cannot identify the gateway model's context window. Launch with an explicit model and [1m] if the route supports it; use an accurate modelOverrides mapping or recognized versioned model name for model identity. See model selection. |

Try it without changing anything

./scripts/sandbox.sh                                          # as a direct user
SANDBOX_GATEWAY_URL=https://your-gateway ./scripts/sandbox.sh # as a gateway user

Points CLAUDE_CONFIG_DIR and STACKED_CONFIG_DIR at temporary directories, runs against them, and deletes everything on exit. Your real config is never read or written. Pass any subcommand to run that instead of setup.

Known limits

  • Rotation happens between sessions, not inside one. Auth is read at startup, so stacked rotates and relaunches with --continue after Claude exits. The watcher tells you when to exit, but the exit is yours to make; it is not a seamless mid-conversation swap.
  • Each probe is a real API call. status costs one per account, and each poll of a wait costs one per account.
  • --continue resumes the most recent conversation in that directory. After a multi-hour wait that is no longer a precise handle: if anything else ran claude there overnight, the resume lands on the wrong conversation. Do not leave two waiting runs in the same directory.
  • A sleeping laptop resumes at wake, not at the reset. Use caffeinate -is.
  • macOS only.
  • The quota-pool check is a heuristic. It is right on every case tested, but it reasons from reset windows rather than an identity the API will confirm.

Testing

./tests/run_all.sh

The suite is fixture-driven and stubs external commands, so it should not read your real Keychain, call Claude Code, or consume quota. STACKED_SIMULATE_EXHAUSTED=<account> forces a failover so rotation can be exercised without waiting for a real limit. The wait tests pair STACKED_SLEEP_CMD with STACKED_SIMULATE_CLOCK — a stubbed sleep that returns at once and pushes a virtual clock forward — so an eight-hour wait is covered in milliseconds.

Contributor notes live in CONTRIBUTING.md.