Monitor your Claude AI usage with a macOS menu-bar widget. Polls the Anthropic API with long-lived OAuth tokens (one or many) and surfaces quota, reset times, and usage trends without leaving your menu bar.
Anthropic doesn't expose a documented public API for checking consumer subscription usage (Pro/Max). The web dashboard at https://claude.ai/settings/usage shows your limits but has no programmatic equivalent.
Claude Monitor calls the same internal endpoints that Claude Code uses, using OAuth tokens you provide, and renders the data locally on your Mac.
- Live usage % in the menu bar (Stats-app style), color-coded — orange at 90%, red above 95%.
- Summary table popover. All accounts at a glance: session %, weekly %, reset times, data freshness, token health, and a one-click chart launcher per row.
- Headroom score (0–100). A single number —
100 − max(session %, weekly %)— answering "which account should I be using". Default sort. - Click any column header to sort. Account, Headroom, percents, reset times, freshness, token status. Chevron marks the active column; click again to flip direction. Rows without data sort to the bottom.
- Pin which account drives the menu bar. Radio button on each row. Click to pin; click again to revert to auto-pick (most-available). Pinning survives restarts.
- Per-row history charts. Click the chart icon to open a usage-history window for that account; the popover stays open so you can open several side-by-side and compare.
- Multi-account. Add accounts one-by-one via a token from
claude setup-token, or bulk-import from a.envfile withACCOUNT_EMAIL_N/ACCOUNT_KEY_Npairs. - Multi-provider. Anthropic and OpenAI/ChatGPT (Codex) accounts sit side by side in the same table, each row tagged with a pixel-art provider badge. See Adding an OpenAI (Codex) Account. A provider that reports no session window (ChatGPT often reports only a weekly one) shows "—" rather than a fabricated 0%.
- Per-model sub-limits. OpenAI accounts report per-model limits alongside the account-level window; these are stored and overlaid on the per-account history chart. The overlay is hidden for accounts that have none.
- Roll Token wizard. Guided revoke-all + re-mint for an account's long-lived token (right-click its row → "Roll Token…"). A temporary stopgap until Anthropic ships a token-management API — see Rolling a Token.
- All data stored locally in SQLite at
~/.claude-monitor/usage.db.
- macOS 13+ (Ventura or later)
- Claude Code installed (you
use it to generate the OAuth tokens via
claude setup-token)
Grab ClaudeMonitor.zip from
Releases, unzip, and
move ClaudeMonitor.app to /Applications.
First run: right-click → Open (required for unsigned apps).
Run this in a terminal:
claude setup-tokenIt opens a browser, asks you to authorize, and prints a token like
sk-ant-oat01-…. These tokens are long-lived (~1 year) so a single
authorization powers the menu bar for the entire lifetime — no refresh dance
needed. If you ever need to revoke and replace one (e.g., after a leak), see
Rolling a Token.
- Click the menu-bar widget.
- Click + Add Account in the footer.
- Paste the token, click Add Account.
Your usage data shows up in the menu bar immediately.
If you maintain tokens for several accounts (e.g., in a .env for an agent
pool), use the Bulk Import field in the Add Account dialog
(.env.example in the repo is a starter template):
ACCOUNT_EMAIL_1=you@example.com
ACCOUNT_KEY_1=sk-ant-oat01-...
ACCOUNT_EMAIL_2=agent@example.com
ACCOUNT_KEY_2=sk-ant-oat01-...Point the importer at the file path; each pair is validated via a ping and added on success. Pinning, sorting, and per-account charts work the same way regardless of how the accounts were added.
Instead of importing by hand, keep a master list that the app loads every time it starts:
~/.claude-monitor/accounts.env— the master list (shared source of truth)~/.claude-monitor/accounts.local.env— local overrides and additions (keep this machine-specific; don't share it)
Both use the same ACCOUNT_EMAIL_N / ACCOUNT_KEY_N format. At launch the app
merges them (the local file overrides the master token for a matching email
and appends any new emails), then imports the result. Loading is
additive: accounts in the lists are added or have their token refreshed, but
accounts already in the app that aren't listed are left untouched — nothing is
removed. Store these files with chmod 600; they contain live tokens.
Tokens in these lists are Anthropic-only. They may also carry keyless Codex identity entries — the intended-set declaration described in Declaring which identities a host should have — which is how you declare an intended set on a headless host with no popover to paste into:
ACCOUNT_EMAIL_3=agent3@example.com
ACCOUNT_PROVIDER_3=openai
ACCOUNT_HOME_LABEL_3=agent3No ACCOUNT_KEY_3 — there is no credential to carry. On every launch this
creates the placeholder if it isn't there and does nothing at all if it is
(including after the identity has actually been provisioned), so it is safe to
keep in a shared master list across the whole fleet.
ChatGPT subscription accounts (Plus/Pro, the ones Codex CLI uses) are polled too, and no inference request is burned to read their usage.
How the reading is taken. There are two ways to ask, and the poller tries them in order, preferring whichever touches the fewest credentials. This app stores no OpenAI credential of its own (#104) — both tiers read through whatever the Codex CLI already owns:
-
codex app-server(preferred). The app runscodex -s read-only -a untrusted app-server, speaks JSON-RPC over its stdio, and readsaccount/rateLimits/read. Codex owns the credential end to end — this app never reads, stores, or refreshes an OpenAI token on this path. Requirescodex0.147.0 or newer: the method does not exist in 0.46.0 (the version Homebrew's formula lags at), which answers-32600instead. Install the current CLI withnpm i -g @openai/codex; it shipslinux-x64/linux-arm64binaries, so this works on headless Linux hosts too.Homebrew ships two
codexpackages, and it's easy to end up on the wrong one:brew install codexinstalls the formula, which is the stale 0.46.0 build above. The current 0.147.0+ CLI is the cask. If you're already on the formula, switch with:brew uninstall --formula codex && brew install --cask codexbrew uninstall --formulamay autoremove now-unused dependencies pulled in only for the formula (observed: it tookripgrepwith it on one host) — reinstall anything you still want separately. The cask also does not self-update in the background (auto_updatesisnullin its cask definition, unlike a cask that manages its own updater) —brew updatealone won't pull in a newcodexrelease, so re-runningbrew upgrade --cask codexperiodically is on you. -
auth.jsonat request time.GET https://chatgpt.com/backend-api/wham/usage— the same endpoint Codex CLI's own/usagecommand calls — with the bearer read fresh out of$CODEX_HOME/auth.jsonfor that one request. Never written back, never refreshed.
A tier that is merely unavailable — no codex on the host, a codex too old,
no readable auth.json — falls through silently to the next one. Only a genuine
failure of the last tier marks the account unhealthy: there is no
stored-credential fallback below it any more, so a home that isn't logged in
(or a host with no codex binary and no readable auth.json) shows up as a
red Token dot rather than quietly polling a stale copy of the credential.
codex is located by absolute path, first hit wins: $CLAUDE_MONITOR_CODEX_BIN,
then each PATH entry, then /opt/homebrew/bin, /usr/local/bin,
~/.local/bin, ~/.npm-global/bin. (A macOS app launched from Finder inherits
launchd's minimal PATH, which contains neither Homebrew's nor npm's bin
directory — hence the explicit list.) Set CLAUDE_MONITOR_CODEX_BIN to point at
a specific install.
Registering an account: one CODEX_HOME per account. codex login writes a
single auth.json per home directory, so each login overwrites the previous
account's credential — which is why monitoring more than one Codex account
never worked before. Give each account its own home and register it by that
path. The one-command way:
claude-monitor codex provision workprovision <label> collapses "pick a home, log in, register it" into one
step: it creates (or reuses, if already present) ~/.codex-<label> as that
identity's CODEX_HOME, drives codex login --device-auth against it
interactively, and on success registers it exactly as codex add --home
does — reusing the same registration code, not a parallel implementation.
Re-running it for a label that's already logged in skips the login step and
just re-registers (idempotent — it will not create a second account row), and
it fails clearly, before touching anything, if <label> is missing or
codex itself can't be found. If the home is already registered to one
account but is now logged in as a different one, it fails rather than
silently repointing the registration — log out and back in with the intended
identity, or provision a different <label>.
That one command is equivalent to the three manual steps it replaces:
CODEX_HOME=~/.codex-work codex login --device-auth
claude-monitor codex add --home ~/.codex-workReach for the manual form when you want the steps decoupled — e.g. running
codex login --device-auth on one machine and codex add --home on another
that shares the same CODEX_HOME over a network filesystem. Either way,
--device-auth prints a code you paste into a browser on any machine, so this
works on a headless Linux host with no browser at all. Repeat for as many
accounts as you have; each poll then spawns codex with that account's own
CODEX_HOME, so one account's numbers can never land on another's row.
codex add reads, copies, and stores no token. It reads exactly one field
out of <home>/auth.json — the opaque account_id that keys the account row —
and asks codex itself for the identity and usage. The credential stays where
Codex CLI put it.
claude-monitor codex list
# ACCOUNT PLAN AUTH CODEX_HOME
# user-3f2… pro logged in /Users/you/.codex-work
# user-91a… plus needs login /Users/you/.codex-personal
# user-77c… pro drift → user-0d4… /Users/you/.codex-spare
# openai-b… — absent (not provisioned on this host)
# → claude-monitor codex provision agent3list reports each home's live state: logged in, needs login (the home
exists but codex login hasn't been run in it), home missing (the directory
is gone — re-register), drift (see below), absent (see
Declaring which identities a host should have),
or unknown when codex itself is absent or too old. Both commands take
--db <path> to work against a throwaway store.
drift means that home is now logged in as a different account than the row
it was registered against — someone ran codex login in it again with another
identity. The poller has always refused to attribute such a reading to the wrong
account; list names it here, and names the account id the home currently holds
(or prints a bare drift when the home's auth.json carries no account id and
only the email disagrees — an email is never printed). The popover names the
same condition on the affected row: an orange dot instead of the usual
green/red/gray, with the identity and remediation in the hover text, and the
row's percentages stop updating rather than presenting stale numbers as current.
Fix it by logging the home back in as the original account, or by re-running
codex add --home <home> to register it as its own account — either way, the
row clears back to normal on the next poll with no restart needed.
An account with no registered home reads the ambient $CODEX_HOME (else
~/.codex), exactly as before — which is correct as long as it is the only
OpenAI account on the host. Add a second OpenAI account and any account still
lacking its own home stops using the ambient one (it can speak for only one of
them, and nothing says which); register its home to bring it back. (A merely
declared identity — see immediately below — is not a second account for this
purpose: it has no login here, so it never triggers that ambiguity.)
Homes are host-local and are never synced. codex login writes a
credential into one CODEX_HOME on one machine; copying it elsewhere is
explicitly a non-goal (see Multi-Host Sync — OpenAI
supports one auth.json per machine and rotates the refresh token on every
use, so two hosts sharing a copy just invalidate each other). Every host must
run codex provision <label> for itself, once per identity.
That leaves a gap worth naming: with three Codex identities across a fleet, a host that was only ever provisioned with two looks exactly like a host that was supposed to have two. There is no missing row to notice — the identity simply isn't there.
So the set of identities a host is expected to have travels on the account copy/paste you already use. There is no config file and no new command.
- On a host that has the identities, click Copy in the popover (or run
the equivalent export). Anthropic accounts travel with their token as
always; each Codex account travels as an identity only — its email, its
provider, and its home label (the
<label>half of~/.codex-<label>), with no key. - On the host that should have them, click Paste. Each identity that
isn't already present becomes a placeholder: an account row with no
credential and no home. (On a headless host with no popover, put the same
keyless entries in
~/.claude-monitor/accounts.env— see Master account list.)
No credential of any kind crosses in either direction, and neither does a home path (a path names a user; only the label you chose travels). This is purely a naming layer over data the app already had.
A declared-but-unprovisioned identity is then visible everywhere, without you having to go looking:
- In the popover — greyed, with an
absentbadge, and every percentage blank. It is never auto-selected for the menu bar and never ranks as "most available" on its empty reading. - In
codex list— statusabsent, with the exact remediation beside it:→ claude-monitor codex provision agent3. - In
ranking.json—"absent": truealongside"status": "blocked", so an external load balancer excludes it whether or not it understands the new key (see Ranking Export).
Run the printed codex provision <label> on that host and the placeholder
converts in place into a real, polling account — same row, no duplicate,
no cleanup step. Nothing else has to be told.
Two things this deliberately does not do:
- Pasting never deletes a Codex account. An identity-only entry says "this host should have this identity"; it is not a claim that any identity missing from the payload is unwanted. A host with an extra provisioned identity keeps it, untouched and unflagged. (Anthropic entries keep their existing replace semantics, since they carry a full credential.)
- Declaring an identity you already have changes nothing. The paste resolves onto the existing row and leaves its home and credential alone, so pasting the same payload twice — or pasting it back onto the host it came from — is a no-op.
Importing a credential instead (codex import)
The original path still works, for hosts without a usable codex binary:
codex login
claude-monitor codex import # or: Add Account → "Import Codex Account"The importer reads $CODEX_HOME/auth.json when CODEX_HOME is set, otherwise
~/.codex/auth.json. Pass --auth <path> to read a different file. The
credential is validated against the live usage endpoint before it's stored, and
the account's email, plan, and OpenAI account id all come back in that same
response — there's no separate profile call.
Prefer codex add: it stores no token at all. codex import still stores one
transiently to validate the credential and identify the account, but a
healing migration nulls it out again on the app's very next launch (#104) —
ongoing polling reads through the tiers above, not the stored copy.
What differs from an Anthropic row once it's added:
- Session % may be blank. OpenAI reports its windows as
primary_window/secondary_window, each carrying its own length, and a ChatGPT Pro account can legitimately report only a weekly window. When there's no session window, the Session cells show—. That is a real "unknown", not 0% — the headroom score and sorting use whichever windows actually exist. - Premium % / Extra are always
—. Those columns track Anthropic premium tiers; the Fable probe is skipped for OpenAI accounts entirely. (The premium column is titled "Fable %" only when every account in the table is Anthropic; with any OpenAI row present it shows the neutral "Premium %".) - This app stores no OpenAI credential, and renews nothing. Usage is read
via
codex app-server(tier 1) or a one-timeauth.jsonbearer read (tier 2); either way the Codex CLI owns the credential and its own renewal entirely, and this app touches neither. The Token dot reports the outcome: green (a tier read succeeded), red (every tier failed — most often the home isn't logged in; hover for the reason, then runcodex loginor register the right home withclaude-monitor codex add --home <path>). A stale OpenAI account never fails silently.
Historical note. Earlier versions stored an OpenAI access/refresh token of their own and proactively renewed it ahead of expiry. OpenAI rotates the refresh token on every renewal and supports exactly one
auth.jsonper machine, so any second copy — another host's database, or the Codex CLI's ownauth.json— took turns invalidating each other's copy on every renewal (observed in practice: continuous401 / refresh_token_invalidatedacross two hosts for ~9 days). That stored-credential path is gone (#104): this app now only ever reads throughcodex app-serveror a liveauth.jsonbearer, so there is no copy of the credential left for it to invalidate. Full history in the 2026-08-15 supersession indocs/spikes/2026-07-30-codex-usage-probe.md.
Temporary workaround. Anthropic exposes no supported API to list, revoke, or programmatically mint long-lived
sk-ant-oat01tokens, so this workflow leans on undocumented claude.ai internals plus manual browser steps. It should be revisited (and ideally replaced) once Anthropic ships a real token-management API — see anthropics/claude-code#43801 (revocation doesn't reliably invalidate tokens), #22995 (token/session management dashboard request), #48373 (claude setup-token --list/--revokerequest), and #59378 (per-session token minting).
If a token leaks — or you just want to rotate one — right-click the account's row in the popover and choose "Roll Token…". A per-account wizard window opens (its header shows a "Last rolled …" timestamp) and walks you through four steps:
- Log in as this account. A button opens
https://claude.ai/settings/claude-codein your browser; make sure the browser is signed in as the account being rolled. - Revoke the old tokens. A button copies a browser-console script with the account's org id baked in. Paste it into the browser console (⌥⌘J) on the logged-in claude.ai page and press Return. It revokes every authorization token on the account — this signs out all devices using it.
- Mint a new token. Run
claude setup-tokenin a terminal (copy button provided), complete the browser login, and paste the printedsk-ant-oat01-…back into the wizard. The wizard verifies the token and rejects it if its org id doesn't match the account being rolled — a guard against accidentally pasting a different account's token into the wrong roll. - Verify the old token is revoked. After a successful import, the wizard pings the token this account had before the roll. If the API rejects it with 401 you get "Revoked ✓"; if it still answers (200 or 429 — both mean the token still authenticates) you get "Still valid!"; a network or server error shows "Couldn't check".
The console script hits internal claude.ai endpoints with no public, documented equivalent:
- List tokens:
GET https://claude.ai/api/oauth/organizations/{org}/oauth_tokens - Revoke one:
POST https://claude.ai/api/oauth/organizations/{org}/oauth_tokens/{id}/revoke
Both authenticate with the claude.ai web session cookie
(credentials: 'include'), not the Bearer token — the OAuth token itself gets
account_session_invalid. That's why revocation can only run pasted into a
browser console on a logged-in claude.ai page, never from the app itself.
Minting can't be automated either: claude setup-token requires interactive
browser OAuth.
The script is defensive about known flakiness: it re-lists live tokens between revoke rounds and retries stragglers for up to 10 rounds, and a 403 on the list call means the browser is logged into a different account (the script aborts with a clear error).
Because these endpoints are undocumented, they may change without notice. If a
roll stops working, the script template in TokenRoller.revokeAllScript
(menubar-app/ClaudeMonitor/Sources/TokenRoller.swift) is the single place to
update.
Finally, server-side revocation is known to lag or silently fail (see anthropics/claude-code#43801 above) — which is exactly why step 4 exists: the app independently verifies the old token with its own ping rather than trusting that the revoke succeeded.
The whole app is just a wrapper around a single, cheap API call. With a
sk-ant-oat01-… token from claude setup-token you can fetch the same usage
data the menu bar shows, using only curl. Both a 200 (Haiku reply) and a 429
(rate-limited) response carry the usage data in headers:
TOKEN='sk-ant-oat01-...' # from `claude setup-token`
curl -sS -D - -o /dev/null https://api.anthropic.com/v1/messages \
-H "Authorization: Bearer $TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "User-Agent: claude-code/2.0.37" \
-H "Content-Type: application/json" \
-d '{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{"role":"user","content":"x"}]}' \
| grep -i '^anthropic-'The interesting headers in the response:
| Header | Meaning |
|---|---|
anthropic-organization-id |
Which account this token belongs to |
anthropic-ratelimit-unified-5h-utilization |
Session quota used (0.0–1.0) |
anthropic-ratelimit-unified-5h-reset |
Session reset (epoch seconds) |
anthropic-ratelimit-unified-7d-utilization |
Weekly quota used (0.0–1.0) |
anthropic-ratelimit-unified-7d-reset |
Weekly reset (epoch seconds) |
anthropic-ratelimit-unified-status |
allowed / allowed_warning / etc. |
Each call costs ~1 Haiku output token (effectively free). The
oauth-2025-04-20 beta flag is what lets the OAuth-issued token authenticate
against /v1/messages.
┌──────────────────────────────────────────────────────────────────────────┐
│ OAuth credentials │
│ - `claude setup-token` (single, paste-in) [anthropic] │
│ - `.env` bulk import (ACCOUNT_EMAIL_N / ACCOUNT_KEY_N) [anthropic] │
│ - `claude-monitor codex import` (~/.codex/auth.json) [openai] │
│ - `claude-monitor codex add --home <path>` (no token) [openai] │
└────────────────────────────────┬─────────────────────────────────────────┘
│ stored in
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ SQLite — ~/.claude-monitor/usage.db │
│ accounts │ oauth_credentials │ usage_history │ settings │
│ (accounts.provider / oauth_credentials.provider tag the upstream) │
└────────────────────────────────┬─────────────────────────────────────────┘
│ read by
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ OAuthPoller — one client per provider, one shared window model │
│ anthropic: POST /v1/messages (1-token ping) → rate-limit headers │
│ tokens are long-lived (~1 yr); no refresh dance needed │
│ openai: codex app-server (preferred) or a live auth.json bearer → │
│ rate_limit.{primary,secondary}_window; no OpenAI token is │
│ stored or refreshed by this app (#104) │
│ - Each account polled once per 10 min (staggered) │
└────────────────────────────────┬─────────────────────────────────────────┘
│ data drives
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Menu-bar app (SwiftUI) │
│ Left-click → summary-table popover │
│ Right-click → quick account switcher (opens chart for the pick) │
└──────────────────────────────────────────────────────────────────────────┘
- macOS 13+
- Xcode Command Line Tools (
xcode-select --install) - Claude Code (to generate tokens)
git clone https://github.com/rjwalters/claude-monitor.git
cd claude-monitor/menubar-app/ClaudeMonitor
swift build
.build/debug/ClaudeMonitor &CI (.github/workflows/build.yml) runs on pushes to main and on pull requests:
two jobs build the package — macOS and a swift:6.1 Linux container — and each
runs ClaudeMonitor selftest, with the Linux job also smoke-running --once.
Manually re-triggering CI. The push/pull_request webhook deliveries
that normally queue a run can silently stop firing for a stretch of time —
with no change to the workflow file, Actions settings, or repo state (see
#66). Since the
workflow also carries a bare workflow_dispatch: trigger, you can queue a run
directly against any branch (including an open PR's head) without depending
on that webhook:
gh workflow run build.yml --ref <branch-name>or use the Actions tab's "Run workflow" button. If checks are missing on an
open PR and re-running doesn't help, next check (needs repo-admin access):
Settings → Actions → General (Actions permissions toggle), Settings →
Webhooks → Recent Deliveries (look for failed/missing pull_request
deliveries), and https://www.githubstatus.com/history for an Actions/Webhooks
incident.
./scripts/build-macos-app.shThe script auto-detects the installed claude-code npm version and patches
the User-Agent string in AnthropicAPI.swift before compiling. Output:
build/ClaudeMonitor.app and build/ClaudeMonitor.zip.
When replacing /Applications/ClaudeMonitor.app, you must rm -rf the old
bundle before copying — cp -R over a running app does not replace the
binary. See CLAUDE.md for the exact sequence.
The same package builds on Linux as a headless daemon — no UI, same poll loop
(account-file sync, 10-minute usage pings, 20-minute Fable probes) writing the
same ~/.claude-monitor/usage.db and ranking.json. This is what Loom hosts
run.
Requires a Swift toolchain (swift.org or the
swift:6.1 Docker image) and the SQLite dev headers:
sudo apt-get install libsqlite3-dev # (yum: sqlite-devel)
cd claude-monitor/menubar-app/ClaudeMonitor
swift build -c release
sudo cp .build/release/ClaudeMonitor /usr/local/bin/claude-monitorPut your accounts in ~/.claude-monitor/accounts.env
(ACCOUNT_EMAIL_N / ACCOUNT_KEY_N pairs, same format as the app's bulk
import — see Multiple Accounts), then:
claude-monitor # poll loop, logs to stdout + ~/.claude-monitor/debug.log
claude-monitor --once # one poll cycle, write ranking.json, exit
claude-monitor --interval 300 # override per-account poll interval (seconds, min 60)
claude-monitor --version # print the version and exit
claude-monitor selftest # self-check (no network/credentials); non-zero exit on failureselftest also takes --db <path> (migrate and verify a copy of a real
database — it writes, so never point it at the live usage.db) and
--wire <path> (decode a captured /wham/usage body offline to re-check the
OpenAI wire contract; prints only derived numbers, never identity fields).
Run claude-monitor selftest --help for details.
Edits to accounts.env / accounts.local.env are picked up automatically
while the daemon runs. A sample systemd user unit is provided at
scripts/claude-monitor.service.
On macOS the same headless loop is available as ClaudeMonitor --headless
(the bare binary or the app bundle's Contents/MacOS/ClaudeMonitor).
ClaudeMonitor --version prints the version and exits on macOS with or
without --headless — it never launches the GUI. --once and --interval
are headless-loop flags: bare (without --headless) they print an error to
stderr and exit non-zero rather than launching a duplicate GUI instance, e.g.
ClaudeMonitor --headless --once.
When multiple hosts each run their own claude-monitor (e.g. two Macs + a
fleet of headless Linux workers), account records and OAuth credentials added
on one host don't automatically appear on the others. claude-monitor accounts export / import is the blessed, headless-safe way to converge
them — no GUI required, works identically on macOS and Linux:
# On the source host: dump account records + credentials to a file (0600).
claude-monitor accounts export --output accounts.json
# Copy it to each destination host over a trusted channel (scp, etc.),
# then converge that host's own usage.db:
claude-monitor accounts import accounts.json- What's synced: Anthropic account identity (id, name, email, plan) and OAuth credentials (access/refresh tokens, expiry, scopes, plan tier). Usage history, rankings, and poll status are not synced — those stay local to each host's own polling.
- Codex/OpenAI accounts are host-local and excluded on purpose:
exportnever emits one, andimportskips (rather than errors on) any it finds in a bundle from an older version. Codex usage is read viacodex app-serveragainst a per-accountCODEX_HOME, and OpenAI supports exactly oneauth.jsonper machine — shipping a copy of that credential to another host only guarantees the two hosts take turns invalidating each other's copy. Register a Codex account on each host instead:claude-monitor codex provision <label>. To carry across which identities a host should have (names only, still no credentials), use the popover's Copy/Paste — see Declaring which identities a host should have. - Idempotent, upsert-by-email:
importmatches accounts by email (falling back to id when email is absent), creates any account it doesn't find locally, and updates the rest — except it never regresses a newer local record: if the locallast_updatedis at least as recent as the imported one, that account is left untouched. Safe to re-run against the same file, and safe to import an older export after newer local polls.--dry-runpreviews the account count without writing anything. - Credentials are secrets: the export is plaintext JSON containing live
OAuth tokens.
--output <path>writes it with0600permissions and the command prints a warning either way; without--output(stdout, e.g. for> accounts.json) permissions aren't set for you —chmod 600the result, transfer it over a trusted channel, and delete it once every destination host has imported. Full at-rest/in-transit encryption (age, openssl) is a natural next step but out of scope for the first pass here. - No
--headlessflag needed —accounts export/importare one-shot operations, reachable directly on both platforms even from the macOS GUI build:ClaudeMonitor accounts export ....
Run claude-monitor accounts --help for the full flag list.
After every poll cycle the app writes a small, non-secret, email-keyed
snapshot to ~/.claude-monitor/ranking.json for external multi-account load
balancers (notably loom-daemon, which uses it to pick a token). It's written
atomically, so a reader never sees a partial document.
Schema change for consumers (added with OpenAI support):
provideris new and additive. It is emitted for every account and is"anthropic"or"openai"; accounts that predate multi-provider support report"anthropic".schemastays 1 on purpose — the version number tracks breaking changes, and adding an optional key breaks nobody. A consumer that ignoresproviderbehaves exactly as it did before. Treat an unrecognized future value as "some other provider" rather than rejecting the document.utilization["5h"]can be absent on anopenaiaccount. The ChatGPT usage endpoint may report a weekly window and no session window at all. A missing key means unknown, not0.0— reading it as zero would make an account look like it has full session capacity. The same applies toresets["5h"]. (Anthropic accounts always report both windows.)absentis new and additive. It appears only on an OpenAI identity this host is expected to have but was never provisioned with (see Declaring which identities a host should have); the key is omitted entirely for every other account, so nothing changes for a consumer that has never heard of it. Such an account is emitted with"status": "blocked"— an existing value that already means "do not route work here" — and with noutilization,resets, orupdated_at, since nothing has ever been read for it locally. A consumer that ignoresabsenttherefore still excludes it correctly; one that reads it can tell "the credential here is broken" (blocked) apart from "this host was never set up for that identity" (blocked+absent), which is what makes a fleet-wide "who is missing which account?" view possible at all.schemastays 1.- Accounts with a
NULLemail are still excluded entirely;emailremains the sole join key, and it is not unique across providers — one person's Anthropic and OpenAI accounts can share an address, distinguished byprovider. - No credential material ever appears in this file, and neither does a
CODEX_HOMEpath.
mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/com.claude-monitor.plist << 'EOF'
<?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.claude-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/ClaudeMonitor.app/Contents/MacOS/ClaudeMonitor</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
</dict>
</plist>
EOF
launchctl load ~/Library/LaunchAgents/com.claude-monitor.plistTo remove auto-start:
launchctl unload ~/Library/LaunchAgents/com.claude-monitor.plist
rm ~/Library/LaunchAgents/com.claude-monitor.plistNo data yet. Click the widget → + Add Account → paste a token from
claude setup-token.
The import dialog calls refreshAll after a successful add, so all 10/13/etc.
accounts should appear when the popover reopens. If a token dot stays gray,
the next 30 s poll tick hasn't filled in token status yet — click the
↻ refresh icon in the popover header.
Anthropic-side revocation is known to be unreliable or slow to take effect (anthropics/claude-code#43801), which is why the Roll Token wizard's final step pings the old token itself — still answers (200/429) means still valid, rejected (401) means revoked — instead of trusting the revoke call. If the badge says "Still valid!", re-run the revoke console script from step 2 (it automatically retries tokens that survive a round) and click Verify old token revoked again. "Couldn't check" means the ping itself failed (network/5xx) — try again later.
~/.claude-monitor/debug.log
~/.claude-monitor/usage.db
Query directly:
sqlite3 ~/.claude-monitor/usage.db "SELECT email, last_updated FROM accounts;"
sqlite3 ~/.claude-monitor/usage.db \
"SELECT timestamp, primary_percent FROM usage_history ORDER BY timestamp DESC LIMIT 10;"accounts table contract for external consumers: email is the stable
join key external tooling should key off of — notably loom-daemon tokens import-from-monitor, which matches accounts by email to build its token
pool. account_name is a free-text, user-editable display label/alias and is
not guaranteed to be an address (though it often is, since renaming an
account to its own email is a common way to tell accounts apart in the UI).
The app backfills email from account_name whenever the profile-derived
email is unavailable but the label is itself a well-formed address — both at
add/rename time and via a one-time healing migration on launch — so no account
with valid credentials should persist indefinitely with email = NULL. If you
ever see one, it means account_name isn't address-shaped either; there's no
address for external tooling to recover.
pkill ClaudeMonitor
launchctl unload ~/Library/LaunchAgents/com.claude-monitor.plist 2>/dev/null
rm ~/Library/LaunchAgents/com.claude-monitor.plist 2>/dev/null
rm -rf ~/.claude-monitor
rm -rf /Applications/ClaudeMonitor.appVersions before 1.8 shipped a Node CLI (dist/cli.js) that was removed in the v1.8 Swift rewrite. Its build artifacts are untracked, so they linger in old checkouts and fail confusingly if run (e.g. node dist/cli.js errors with ERR_MODULE_NOT_FOUND for commander). Clean them up:
rm -rf dist node_modulesclaude-monitor/
├── menubar-app/ClaudeMonitor/ # Swift Package: macOS menu-bar app + Linux headless daemon
│ ├── Package.swift
│ ├── Assets/ # App icon (AppIcon.icns + 1024px master PNG + generation recipe)
│ ├── CSQLite/ # System-library shim mapping Linux libsqlite3
│ └── Sources/
│ ├── main.swift # macOS entry: AppDelegate, menubar icon, popover wiring
│ ├── HeadlessMain.swift # Linux entry (always headless)
│ ├── HeadlessRunner.swift # UI-less poll loop (Linux daemon / --headless on macOS)
│ ├── AccountSync.swift # accounts export/import: multi-host record + credential sync
│ ├── AccountSyncCLI.swift # `claude-monitor accounts export|import` CLI surface
│ ├── CLIArgs.swift # Shared --db/--help parsing for the subcommand CLIs
│ ├── UsageStore.swift # SQLite store, settings, primary-account pin
│ ├── AccountFreshness.swift # Single staleness rule shared by display/ranking paths
│ ├── SQLiteDB.swift # Minimal system-libsqlite3 wrapper (zero deps)
│ ├── UsagePopoverView.swift # Summary table, sortable headers, add-account dialog
│ ├── PercentSeverity.swift # Shared >95 / >=90 severity bands for popover, chart, menubar
│ ├── UsageChartView.swift # Per-account chart window
│ ├── OAuthPoller.swift # Per-provider polling, token add/import/refresh
│ ├── AnthropicAPI.swift # Anthropic client (ping + rate-limit headers)
│ ├── OpenAIAPI.swift # OpenAI/Codex client (wham/usage + token refresh)
│ ├── CodexAppServer.swift # Codex app-server JSON-RPC client (usage with no stored credential)
│ ├── CodexCLI.swift # `claude-monitor codex provision|add|list|import` CLI surface
│ ├── RateLimitWindow.swift # Provider-agnostic window/snapshot model
│ ├── UsageProviderClient.swift # UsageProviderClient protocol + credentials
│ ├── SelfTest.swift # `claude-monitor selftest` portable-core assertions
│ ├── RollTokenView.swift # Roll Token wizard window (rotate long-lived tokens)
│ ├── TokenRoller.swift # Revoke-all browser-console script generator
│ ├── RankingExporter.swift # Emits ~/.claude-monitor/ranking.json for load balancers
│ ├── FileLogger.swift # Debug logging
│ ├── NaturalSort.swift # Hybrid lexical/numeric ordering (agent-10 after agent-9)
│ └── LinuxCompat.swift # ObservableObject/@Published stand-ins for Linux
├── scripts/
│ ├── build-macos-app.sh # macOS release build script
│ └── claude-monitor.service # Sample systemd user unit for Linux headless mode
├── docs/spikes/ # Investigation write-ups (e.g. the OpenAI usage-endpoint probe)
├── .github/workflows/build.yml # CI: build + selftest on macOS and Linux
├── build/ # Build output (gitignored): ClaudeMonitor.app + .zip
├── CHANGELOG.md # Release history
├── CLAUDE.md # Development notes (build/install sequence, invariants)
├── WORK_LOG.md, WORK_PLAN.md # Loom-maintained work history and plan (regenerated by the Guide role)
├── .env.example # Sample accounts.env for bulk import
├── window.png, plot_window.png # README screenshots
├── loom.sh, package.json # Loom orchestration workspace files (not part of the app)
└── .loom/, .claude/, .gitattributes # Loom + Claude Code tooling installs
- ccusage — CLI tool that reads local Claude Code JSONL logs and reports token usage / API-equivalent costs.
- VibePulse — macOS menu-bar app built on ccusage, showing real-time token spend.
How they differ from Claude Monitor:
- ccusage / VibePulse read local Claude Code logs → token counts and cost estimates.
- Claude Monitor queries the Anthropic API via OAuth → quota %, reset times, and headroom across multiple accounts.
MIT
{ "schema": 1, "generated_at": "2026-07-30T18:00:00Z", "accounts": [ { "email": "you@example.com", // the join key "provider": "anthropic", // "anthropic" | "openai" "plan": "max_20x", "status": "available", // available | rate_limited | exhausted | blocked "utilization": { "5h": 0.12, "7d": 0.44 }, // 0.0–1.0 "resets": { "5h": "…Z", "7d": "…Z" }, "models": { "fable": { "utilization": 0.30 } }, // optional "updated_at": "2026-07-30T17:58:00Z" }, { "email": "you@example.com", "provider": "openai", "plan": "pro", "status": "available", "utilization": { "7d": 0.14 }, // note: no "5h" key — see below "resets": { "7d": "…Z" }, "updated_at": "2026-07-30T17:58:00Z" }, { "email": "agent3@example.com", "provider": "openai", "status": "blocked", // never routable "absent": true // …because it isn't set up on this host // no utilization / resets / updated_at: nothing has ever been read here } ] }