Usage limits and instant account switching for Claude, Codex, and Copilot — from one terminal command, turning scattered LLM logins into one coordinated budget.
A command line utility that reads your Claude, Codex, and Copilot usage limits from the same credential stores those tools already use — and switches between stored accounts without a browser round-trip. Model-scoped weekly windows (like 7-day fable above) are shown for visibility whenever Anthropic's usage response scopes a limit to a specific model.
$ aistat -h
Claude usage
- personal@example.com (active) [Max 5x]
- 5-hour: 92% (resets in 4h 53m)
- 7-day: 71% (resets in 2d 5h)
- 7-day sonnet: 58% (resets in 2d 5h)
- 7-day fable: 34% (resets in 2d 5h)
- work@example.com [Max 20x]
- 5-hour: 4% (resets in 4h 12m)
- 7-day: 12% (resets in 5d 9h)
Codex usage
- me@example.com (active)
- 5-hour: 2% (resets in 2h 26m)
- 7-day: 0% (resets in 6d 21h)
- Code review 7-day: 0% (resets in 6d 21h)
Copilot usage
- month: 67.3% (resets in 5d 1h)One command rotates the live credential to the fresher account — no browser round-trip:
$ aistat switch
switched to work@example.com (uuid 1a2b3c4d-…); was personal@example.comaistat is built for usage-aware agent routing: pick which provider to spawn the next subtask on by which one will waste capacity if you don't use it, rather than by which one is your default. The JSON output is the routing primitive — orchestrators read it, score the candidates, and dispatch.
AISTAT-16x9.mp4
Claude spawns a Codex subagent — picked because aistat shows Codex with the most headroom.
Three runnable examples in examples/ put the pattern to work:
agent-selection.md— let your LLM agent manage its own usage quotas, turning your scattered LLM logins into one coordinated budget the agent draws from automatically.route.sh "your prompt"— picks the provider with the most headroom and runs the prompt against that CLI (claude -p/codex exec/copilot -p).auto-switch.sh— rotates the live Claude credential to a fresh stored account when the active one is past a threshold. Drop into Claude Code's~/.claude/settings.jsonas aSessionStarthook to start every session on your freshest slot.
aistat is the input; the workflow is the rest.
Works on macOS, Linux, and Windows (Git Bash + WSL):
curl -fsSL https://raw.githubusercontent.com/drogers0/aistat/main/install.sh | shOther install options
Manual download: grab a tarball (.zip on Windows) from the Releases page and place aistat on your PATH.
Pin a specific version:
AISTAT_VERSION=v2.1.0 curl -fsSL https://raw.githubusercontent.com/drogers0/aistat/main/install.sh | shChoose an install directory:
curl -fsSL https://raw.githubusercontent.com/drogers0/aistat/main/install.sh | sh -s -- --prefix=$HOME/binDon't touch my shell rc:
curl -fsSL https://raw.githubusercontent.com/drogers0/aistat/main/install.sh | sh -s -- --no-modify-pathgo install (requires Go 1.22+):
go install github.com/drogers0/aistat/v2/cmd/aistat@latestaistat # default: same as `aistat usage`
aistat usage [provider] # report usage for all providers, or one (claude | codex | copilot)
aistat switch # bulk: auto-switch every provider with ≥2 stored accounts to its freshest
aistat switch <provider> # switch one provider to the account with the most headroom
aistat switch <provider> --to <id> # switch to a specific stored account (email substring or uuid prefix)
aistat switch <provider> --if-above-5h <N> # conditional: switch only when a threshold is crossed (see below)
aistat switch <provider> --watch # run the conditional switch on a timer, in the foreground
aistat accounts list [provider] # list stored accounts (all providers, or one)
aistat accounts remove <id> [provider] # remove a stored account (provider inferred from id when unambiguous)Switch and accounts work across Claude and Codex; Copilot is single-account (usage-only). --to can omit the provider when the id matches exactly one provider's store.
Flags: -h/--human for text rendering (affects usage and accounts list), --refresh to bypass the per-account usage cache (~90 s TTL, affects usage only), --debug for per-request diagnostics on stderr, --version and --help for the obvious.
Whichever account is active when you call aistat gets stored automatically. After a claude /login or codex login, the next aistat usage adds it alongside the others — no extra setup, no separate command.
aistat accounts list shows every stored account, aistat accounts remove <id> deletes one (the currently-active account is protected — switch away with aistat switch --to <email> or run logout first).
aistat switch is the only command that mutates a live credential; aistat usage is read-only.
aistat switch rotates the live credential to a different stored account — no browser round-trip:
- Auto-pick (
aistat switch): picks the stored account with the most 5-hour headroom. - Explicit (
aistat switch --to <email|uuid>): match by email substring or UUID prefix.
Auto-pick buckets candidates by 5% (so 87% and 89% are equivalent) and breaks ties by most-recent use. It optimizes relative headroom, not "has enough quota for the workload you're about to start" — for nuanced cases, pass --to explicitly.
Note
Multi-account support covers Claude and Codex — Copilot rides on whatever single-account credential its upstream CLI writes.
Warning
aistat switch rotates the credential for the whole device, not per session. Every session or tool sharing that provider's credential (e.g. all your Claude Code windows) picks up the new account on its next read — there's no per-session or per-chat isolation.
Give aistat switch a threshold flag and it becomes conditional: it only switches when the active account has actually crossed that usage threshold — otherwise it prints no switch needed (<window> at <used>%) and exits 0 without touching the credential. It checks the active account's own usage first and only fetches every stored account's usage (the auto-pick step) once a threshold is crossed.
aistat switch claude --if-above-5h 85 # switch only if the 5-hour window is ≥85% used
aistat switch claude --if-above-5h 90 --if-above-weekly off # 5h at 90%, weekly disabled
Two independent thresholds, checked as used-percent (five-hour is checked before the weekly window):
| Window | Flag | Env fallback | Default |
|---|---|---|---|
five_hour |
--if-above-5h |
AISTAT_IF_ABOVE_5H |
85 |
weekly (seven_day / thirty_day) |
--if-above-weekly |
AISTAT_IF_ABOVE_WEEKLY |
95 |
Precedence is per window: an explicit flag wins; otherwise a non-empty env var; otherwise the built-in default. Set any of them to off to disable that window as a trigger. Because the env vars are only a fallback, you can pin one window with a flag and let the other come from the environment.
Add --notify for a desktop notification (macOS only, silent no-op elsewhere) — fired on a successful switch, or when the threshold is hit but no better account is available.
Threshold flags cannot be combined with --to (a usage error, exit 2). Like unconditional switch, a conditional switch fails closed: a fetch or write error exits 1 without mutating the live credential.
Add --watch (or -w) to run the conditional switch on a timer, in the foreground. --watch always notifies, with in-memory notification dedup so a persistent "no better account" state warns you once, not on every tick. It ticks immediately on startup, then every --interval seconds (default 300, minimum 60). --watch implies conditional mode; with no threshold flag the windows fall back to the env vars / defaults above. You keep it alive with your OS's own service manager — it's a long-running foreground loop.
aistat switch --watch # all providers with ≥2 stored accounts, env/default thresholds
aistat switch claude --watch --interval 120 # claude only, checked every 2 minutes
aistat switch --watch --if-above-5h 90 --if-above-weekly off
launchd (macOS) / systemd (Linux) setup
launchd (macOS) — save as ~/Library/LaunchAgents/com.drogers0.aistat.autoswitch.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.drogers0.aistat.autoswitch</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/aistat</string>
<string>switch</string>
<string>--watch</string>
<string>--interval</string><string>300</string>
</array>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/aistat-autoswitch.log</string>
<key>StandardErrorPath</key><string>/tmp/aistat-autoswitch.log</string>
</dict>
</plist>launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.drogers0.aistat.autoswitch.plist # start
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.drogers0.aistat.autoswitch.plist # stop
systemd (Linux) — save as ~/.config/systemd/user/aistat-autoswitch.service:
[Unit]
Description=aistat conditional account switcher
[Service]
ExecStart=/usr/local/bin/aistat switch --watch --interval 300
Restart=always
[Install]
WantedBy=default.targetsystemctl --user enable --now aistat-autoswitch.service
KeepAlive / Restart=always also restart the daemon after a crash or logout — the in-memory notification dedup resets on restart, so you may see one repeat notification right after a restart.
aistat reads from the credential stores each tool already populates. If a credential is missing, the error message names the exact command to fix it.
| Provider | Set up with |
|---|---|
| Claude | claude /login |
| Codex | codex login |
| Copilot | gh auth login + gh auth refresh -h github.com -s user (the user scope is required) |
aistat reads the credentials claude /login, codex login, and gh auth login already wrote, makes one authenticated HTTPS call per provider in parallel, and normalizes each response into a uniform {used_percent, remaining_percent, resets_at} shape. A failing provider doesn't block the others — its error surfaces in the JSON, and a single per-account hiccup on a multi-account provider (Claude or Codex) doesn't flip the overall exit code. Per-account usage is cached for 90 seconds so script-driven polling doesn't burn rate limit.
Endpoints, caching, retries, exit codes, and the JSON contract
Endpoints.
| Provider | Endpoints |
|---|---|
| Claude | api.anthropic.com/api/oauth/usage, api.anthropic.com/api/oauth/profile, platform.claude.com/v1/oauth/token |
| Codex | chatgpt.com/backend-api/wham/usage |
| Copilot | api.github.com/copilot_internal/user |
Caching. Each Claude and Codex account's usage response is cached for 90 seconds so back-to-back invocations don't hammer the upstream rate limits. aistat usage --refresh bypasses the cache; aistat switch reads through it, so refresh first if you want a switch decision based on the freshest numbers. Override the TTL with AISTAT_USAGE_CACHE_TTL=10s (or any duration). If the cache can't be written, the run proceeds without it.
User-Agent. The Claude provider sends User-Agent: claude-code/<version> on the wire — Anthropic's /oauth/usage endpoint aggressively throttles non-claude-code/ clients (anthropics/claude-code#31637). Override with AISTAT_CLAUDE_USER_AGENT=<string> (verbatim, e.g. aistat/2.1.0) to opt back into the honest UA. Sibling vars exist for the other providers (AISTAT_CODEX_USER_AGENT, AISTAT_COPILOT_USER_AGENT); those default to aistat/<version> since their endpoints don't currently appear to partition by UA.
Reliability. Transient HTTP failures (408, 429, 5xx, network errors) retry up to 3 times per request, honoring Retry-After (capped at 10s) and otherwise backing off with jitter. The CLI never blocks longer than 15 seconds per Claude account, and a single per-account failure doesn't flip the overall exit code — it's surfaced in the JSON and resolves on the next run.
Exit codes.
| Code | Meaning |
|---|---|
| 0 | All providers succeeded. |
| 1 | One or more providers failed at runtime. |
| 2 | Usage error: unknown subcommand, unknown provider, malformed flags. |
| 3 | Stdout write error (broken pipe, disk full). |
Diagnostics on stderr. Even without --debug, providers may emit diagnostic lines to stderr. All start with aistat:.
aistat: copilot: quota_snapshots present but "premium_interactions" key missing —
GitHub may have renamed the quota; please file an issue at
https://github.com/drogers0/aistat/issues
aistat: claude: could not capture live account profile (<reason>); rendering live row
without storing — run `claude /login` if this persists across runs
aistat: claude: <email>: stored credential rejected (run `aistat usage` to refresh);
excluded from auto-pick
aistat: claude: refresh endpoint rejected request (<status>: <body-snip>); this is
likely an aistat refresh implementation issue, not your account. Run `claude /login`
to work around it for this account and file an issue at https://github.com/drogers0/aistat/issues
The exit code and stdout payload are unaffected — these are heads-ups that the underlying number may be stale, or that one account is excluded from auto-pick. With --debug, additional per-request and per-provider lines are also written to stderr.
JSON output contract.
{
"checked_at": "2026-05-28T01:00:00+00:00",
"providers": {
"claude": {
"accounts": [
{ "email": "personal@example.com", "plan": "default_claude_max_5x", "active": true, "limits": {...} },
{ "email": "work@example.com", "plan": "default_claude_max_20x", "active": false, "limits": {...} }
]
},
"codex": {
"accounts": [
{ "email": "me@example.com", "plan": "", "active": true, "limits": {...} }
]
},
"copilot": { "limits": { "month": {...} } }
}
}Claude and Codex both use the accounts view — an array of per-account rows (even with a single stored account), where the row with active: true carries the live account's limits. Copilot stays single-account: it emits a top-level limits and no accounts. Every Limit has the same four fields: used_percent, remaining_percent, resets_at (ISO 8601), reset_after_seconds. UUIDs surface in aistat accounts list and aistat switch output — that's where you read them when you want accounts remove <uuid-prefix> or switch --to <uuid-prefix>.
aistat ships a Claude Code skill at skills/aistat/ that teaches an agent when to check usage and how to read the JSON, so it can answer "how much headroom do I have?" and rotate accounts on its own. Install it by copying the directory into your skills folder:
cp -r skills/aistat ~/.claude/skills/ # user-wide
# or, per project:
cp -r skills/aistat <your-repo>/.claude/skills/The skill auto-runs only the read-only aistat usage and aistat accounts list commands. Anything that mutates the live credential — aistat switch — is intentionally left out of its allowed tools, so it still requires your approval.
Issues and pull requests are welcome. Before opening a PR, run go test ./..., go vet ./..., staticcheck ./..., and golangci-lint run ./....
If aistat saves you a tab-switch, a ⭐ helps others find it:
gh api --method PUT user/starred/drogers0/aistat(or just click the star at the top of this page)
MIT © 2026 drogers0