Skip to main content
Glama

claude-usage

A small local daemon that tells Claude Code sessions — and you — how close your Claude account is to its rate limits, and how many tokens the sessions on this machine have actually spent. It polls the same OAuth usage endpoint /usage uses, aggregates Claude Code's own transcript files, and serves both over HTTP on 127.0.0.1. Sessions pick it up automatically through a UserPromptSubmit hook (a one-line nudge when a window gets tight) and on demand through an MCP server. It can also list live sessions and pause or freeze them, locally or from a phone over Tailscale.

The problem

Claude Code tells you nothing about your rate-limit headroom until you hit it, and /usage is a thing you have to remember to type. Meanwhile your token spend is sitting in ~/.claude/projects/**.jsonl in a form nobody reads. claude-usage turns both into something a session can see mid-conversation ("5h window 84%, prefer lighter work") and something you can look at on a dashboard.

  • Rate limits — account-wide, from GET https://api.anthropic.com/api/oauth/usage. Read-only. We never refresh tokens and never call any other Anthropic endpoint.

  • Token spend — per machine, aggregated from transcripts, including subagent transcripts (roughly a third of real token use, and not copied into the parent file).

  • Sessions — which Claude Code processes are running, in which project and worktree, what they have spent, and whether they are paused.

Tokens, not dollars. Cost estimates are deliberately out of scope.

Related MCP server: claudeusage-mcp

Install (60 seconds)

npm i -g @drmuzikbpn/claude-usage
claude-usage install

From a checkout:

npm ci && npm run build
node bin/claude-usage install

install is interactive and confirms each item; --yes accepts the defaults (service on, hook on, MCP on, status line off). Flags:

Flag

Effect

--yes, -y

accept the defaults, no prompts (required when stdin is not a TTY)

--no-service

do not install the launchd/systemd unit

--no-hook

do not register the Claude Code hooks

--no-mcp

do not register the MCP server

--statusline

also set statusLine (only if you do not already have one)

--tailscale

add "tailscale" to config.bind so the daemon listens on the tailnet

--linger

systemd only: loginctl enable-linger so the daemon survives logout

Uninstall: claude-usage uninstall (add --purge to delete config and state too).

What install changes on your machine

Path

What

Mode

~/.config/claude-usage/

config, state, bearer token

dir 0700, files 0600

~/.local/share/claude-usage/versions/<version>/

a copy of the package

~/.local/share/claude-usage/current

symlink → the active version; the unit runs out of this

~/Library/LaunchAgents/com.github.drmuzikbpn.claude-usage.plist (macOS)

LaunchAgent, RunAtLoad, KeepAlive: { SuccessfulExit: false }

0644

~/Library/Logs/claude-usage/daemon.{out,err}.log (macOS)

daemon stdout/stderr

dir 0700

~/.config/systemd/user/claude-usage.service (Linux)

user unit, Restart=on-failure, WantedBy=default.target; logs go to the journal

0644

~/.claude/settings.json

merge-only: four hook groups, optionally statusLine

mode preserved

~/.claude/settings.json.claude-usage.bak

one-time backup, written once and never overwritten

copies the source mode

~/.claude.json

mcpServers["claude-usage"], written by claude mcp add --scope user

0600

XDG_CONFIG_HOME and XDG_DATA_HOME are respected and are baked into the unit file.

Exact hook groups

Install appends one matcher-less group per event to hooks.<Event> in ~/.claude/settings.json. Existing groups are never touched.

Event

timeout (seconds)

Why

SessionStart

5

registers the session (sessionId, pid, cwd, transcript, git common dir)

SessionEnd

5

ends the session

UserPromptSubmit

86400

heartbeat → pause gate → limits nudge

PreToolUse

86400

pause gate, and records the last tool used

{ "hooks": [ { "type": "command",
               "command": "/Users/you/.local/share/claude-usage/current/bin/claude-usage hook",
               "timeout": 86400 } ] }

The 86400 s timeouts exist because a soft-paused session's hook sleeps at the boundary; Claude Code's default 60 s would kill it and let the session run on. The hook's own HTTP deadline is 200 ms — that is what protects your prompt latency.

MCP registration

"mcpServers": { "claude-usage": { "type": "stdio",
                                  "command": "<…>/current/bin/claude-usage",
                                  "args": ["mcp"] } }

Primary path is claude mcp add --scope user claude-usage -- <abs path> mcp, because ~/.claude.json is Claude Code's live state file and running sessions rewrite it. If claude is not on PATH, install falls back to a read-merge-rename(2) at 0600 and warns you to quit running sessions first. No .bak is written for that file — a 0644 backup of a 0600 file would leak it.

Undoing it

claude-usage uninstall          # unit removed, hooks + MCP entry removed, config kept
claude-usage uninstall --purge  # also deletes ~/.config/claude-usage and the versions dir

Uninstall removes exactly the entries whose command is our resolved binary, deletes arrays and the hooks object it created if they end up empty, and never touches your .bak or systemd lingering. It SIGCONTs any hard-frozen processes first.

The hook nudge

Silent while everything is ok. At warn:

⚠ Claude usage: 5h window 84% (resets 14:35), 7d 61%. Prefer lighter work, smaller models, avoid large file reads.

At critical:

🛑 Claude usage: 5h window 96% — resets in 41m. Defer non-urgent work; keep outputs short.

Same (window, status) inside the debounce window stays silent; any status change prints immediately. The hook always exits 0, never exits 2, and prints nothing at all if the daemon is unreachable or slow.

MCP tools

Five read-mostly tools over stdio. Each returns a one-line human-readable headline followed by the raw JSON body.

Tool

What

get_limits

the cached rate-limit array (no network call)

get_summary

limits + per-limit ok/warn/critical + today's tokens — prefer this

get_tokens

token spend, { since?, groupBy? }

get_sessions

live sessions: project, worktree, pid, model, tokens, pause

refresh_limits

force a refetch (rate-limited to one per 10 s)

There is deliberately no pause/resume/freeze tool. Controlling sessions stays with the human.

"Before we start this refactor, check get_summary — if the 5h window is over 80%, plan it out in one pass instead of exploring file by file."

Status line

Prints 5h 42% · 7d 61%, coloured amber/red above your thresholds, ⏸ paused from dashboard · … when the session is soft-paused, and nothing at all when the daemon is down. If you already have a statusLine, install prints the snippet instead of overwriting it:

{ "statusLine": { "type": "command",
                  "command": "/Users/you/.local/share/claude-usage/current/bin/claude-usage statusline" } }

CLI

$ claude-usage status
daemon: running (pid 48213, port 47291, version 0.1.0)
overall: warn
limits:
  session               84%  warn      resets 2026-09-13T23:00:00.000Z
  weekly_all            34%  ok        resets 2026-09-16T13:00:00.000Z
  weekly_scoped:fable   20%  ok        resets 2026-09-16T13:00:00.000Z
today: 1,909 messages, in 7,704 / out 534,151, cache 10,071,412 created / 192,771,920 read
$ claude-usage tokens --since 7d --by project
since 2026-09-06T19:01:38.189Z, grouped by project
  group                input        output      msgs
  /home/dev/alpha         16            28         3
  /home/dev/beta         100           200         1
  TOTAL                  116           228         4

--by project|session|model|day; --since today|all|7d|24h|30m|<ISO-8601>.

$ claude-usage sessions
  session    project        pid      state    pause      msgs  last activity
  3f1c0a52…  foo @ foo-wt2  4242     alive    hard(3)      143  2026-09-13T14:02:51.000Z
  8ad4e017…  foo            4390     alive    -             21  2026-09-13T14:01:07.000Z
rev 812
$ claude-usage pause all --reason "dinner"
paused all (soft) — rule r_k3m7qz4ub2ah6ptc
affected: 3f1c0a52…, 8ad4e017…

$ claude-usage pause session:3f1c0a52-9d64-4f2e-8b71-2c5a0d9e4411 --hard
$ claude-usage resume all
removed 1 rule(s); resumed 3f1c0a52…
$ claude-usage resume --all      # works even with the daemon dead — see below

Scopes: all, project:<gitCommonDir>, session:<sessionId>. Soft pause stops the session at its next prompt or tool boundary (the hook sleeps and re-polls once a second). Hard pause additionally SIGSTOPs the tool subprocesses already running below the session — the work in flight, not the claude process itself, so you keep your terminal. Whatever it would do next is stopped at the same gate soft pause uses.

A long hard pause costs the frozen tool call. Claude Code's tool timeout keeps running while a subprocess is stopped, so a freeze outlasting it (2 minutes by default for Bash) kills the command that was in flight — the session survives and sees a timeout error. Short freezes are lossless; hold one for minutes and expect to lose the running tool.

Other subcommands: serve [--verbose], install, configure, uninstall, mcp, hook, statusline, --version, help.

Remote dashboard (Tailscale)

The daemon can serve a second address on your tailnet so a phone or tablet can watch and control sessions.

claude-usage install --tailscale        # or: edit config.bind to ["127.0.0.1", "tailscale"]
claude-usage configure pairing          # prints the pairing JSON + a terminal QR code
claude-usage configure pairing --json   # JSON only

bind entries are IP literals or the keyword tailscale, resolved at startup to the first IPv4 interface address inside 100.64.0.0/10 (falling back to tailscale ip -4). No tailnet → a warning and loopback only. SIGHUP re-resolves without a restart.

The pairing payload:

{ "v": 1, "name": "alans-mbp", "addr": "100.101.102.103", "port": 47291, "token": "…" }

Security model in five bullets

  • A 32-byte random bearer token is minted at install and stored in config.json (0600). claude-usage configure rotate-token replaces it; every paired device must re-pair.

  • Loopback GET/HEAD needs no token — that is what keeps the hook, the status line and curl localhost zero-config. Everything else — every non-loopback request, every mutating request even from loopback — requires Authorization: Bearer <token>, compared in constant time.

  • The Host header must name an address the daemon is actually bound to (or localhost, or the machine's MagicDNS name); anything else is 421. Any request carrying an Origin header is 403. No CORS headers are ever sent. That is the anti-DNS-rebinding story.

  • No endpoint ever returns a credential. The OAuth access token is read, used for one read-only GET and never stored, logged or serialized; /v1/config redacts the bearer token.

  • Loopback is decided from the socket's remote address, never from a header.

Full threat model: docs/security.md. Wire format and every status code: docs/api.md.

The escape hatch

claude-usage resume --all

This works with no running daemon. It reads sessions.json and pause.json straight off disk, SIGCONTs every recorded pid and clears every rule.

It exists because hard pause SIGSTOPs real processes. If the daemon dies — crash, kill -9, a bad update — while your sessions are frozen, they stay frozen, and a stopped process cannot ask for help. Every other safety net (SIGCONT on SIGTERM, on uninstall, on configure service off, and the orphan sweep at startup) depends on the daemon being alive to run it. This one does not. It is the reason hard freeze is a feature you can trust rather than a footgun.

Configuration

~/.config/claude-usage/config.json, mode 0600, directory 0700. Missing file → defaults. Unknown keys — top level and inside every known section — survive a load/save round-trip. An invalid value is an error naming the key.

Key

Default

Meaning

name

os.hostname()

label the dashboard shows for this machine

port

47291

HTTP port (0–65535)

bind

["127.0.0.1"]

IP literals and/or the keyword tailscale

auth.token

""

bearer token; minted by install, 0600

pollIntervalMs

60000

limits poll interval (1 000 – 86 400 000)

thresholds.warn

80

percent at which a limit becomes warn (0–100)

thresholds.critical

95

percent at which a limit becomes critical; must be ≥ warn

hookDebounceMinutes

10

quiet window for repeated nudges (0–1440)

retentionDays

90

days of aggregates and dedup keys kept (1–3650)

projectsDir

~/.claude/projects

transcript root (leading ~ expanded)

integrations.service

true

what install turned on; configure keeps these in step

integrations.hook

true

integrations.mcp

true

integrations.statusline

false

autoUpdate.enabled

true

auto-update on/off (lands in the next merge)

autoUpdate.intervalMs

600000

release-check interval (1 000 – 86 400 000)

autoUpdate.repo

drmuzikbpn/claude-utilization-mcp

release source; override for forks

events.maxClients

16

concurrent SSE streams (1–4096)

Scriptable edits:

claude-usage configure                              # interactive menu
claude-usage configure hook off                     # service | hook | mcp | statusline | autoupdate
claude-usage configure thresholds --warn 75 --critical 90
claude-usage configure port 47300                   # rewrites and restarts the unit
claude-usage configure rotate-token
claude-usage configure pairing [--json]

Two caveats worth knowing today: hookDebounceMinutes and retentionDays are validated and stored but not yet read back by the hook and the spend store respectively — both use their defaults (10 minutes, 90 days). See the deviations section of docs/api.md.

Auto-update

claude-usage update / rollback, the release pipeline and the update fields in /health are live. /health.update reports { "state": "disabled" } only when auto-update is genuinely off — no current symlink, or configure autoupdate off.

  • Versioning MAJOR.MINOR.<commit-count>+<short-sha>, computed in CI from git rev-list --count HEAD on main.

  • Every green CI run on main tags v<version> and publishes a GitHub Release with the npm pack tarball plus SHA256SUMS; npm publish happens from the same job.

  • The daemon checks releases/latest every autoUpdate.intervalMs (±10% jitter), downloads over TLS, verifies the sha256, extracts to versions/<v>/, smoke-tests node bin/claude-usage --version from it, atomically repoints current and exits 0 — KeepAlive / Restart brings it back. The previous two versions are kept; claude-usage rollback repoints current to the previous one.

  • Never restarts while a session is hard-frozen (state: "deferred", deferredReason: "hard_frozen_sessions"), re-checked every 30 s. Soft pauses do not defer.

  • Disabled automatically when there is no current symlink (i.e. you did not use install), or with claude-usage configure autoupdate off.

  • Signature verification (minisign) is a follow-up; today integrity rests on sha256 over TLS from the same release.

  • The Android dashboard ships its APK from this same repo under deck-<version> tags, published as pre-releases so they are never releases/latest. The updater also skips any deck- tag outright, reporting no update rather than an error — an error would mask the next real daemon release, since releases/latest returns only one.

Installing on a remote machine

claude-usage install over ssh works, but macOS gives you a service launchd will not supervise: a LaunchAgent bootstrapped from a non-GUI session has its spawns pended, so RunAtLoad and KeepAlive never fire. The daemon runs, and auto-update restarts it, but launchd will not bring it back after a crash or a reboot. install detects this and says so. To get a self-healing service, run install from a terminal on that machine's own desktop — over Screen Sharing if need be.

Platform support

  • macOS — launchd; credentials from the Keychain item Claude Code-credentials.

  • Linux — systemd user unit; credentials from ~/.claude/.credentials.json.

  • Windows — not supported yet. Install fails preflight; the credential and service-manager interfaces exist so it can be added without touching the daemon.

Node ≥ 20. The daemon itself has zero runtime dependencies — @modelcontextprotocol/sdk and qrcode-terminal are loaded lazily by mcp and configure pairing only.

Troubleshooting

The daemon is down. claude-usage status prints the service state and the last 20 stderr lines when it cannot reach the daemon:

daemon: not running (stale daemon.json — pid 48213 is gone)
config: /Users/you/.config/claude-usage
service: launchd — stopped
log: last 20 stderr lines
  …

macOS logs live at ~/Library/Logs/claude-usage/daemon.err.log (truncated at install if over 10 MB, not rotated otherwise). Linux logs go to the journal: journalctl --user -u claude-usage -n 50.

Keychain prompt. The first credential read raises a macOS Keychain prompt for Claude Code-credentials. install warns you before triggering it. Allow it once ("Always Allow" if you would rather not see it again). Denying it is not fatal: the daemon still runs, spend still works, and /v1/limits reports error.code: "no_credentials".

Port already in use. serve exits 1 with port 47291 is already in use — another claude-usage daemon may be running. Either stop the other one, or claude-usage configure port 47300 (which rewrites and restarts the unit).

Limits say unauthorized. Your OAuth token expired. Run claude to re-login — we never refresh tokens. The poller backs off to one attempt per 10 minutes until a fetch succeeds.

Nudges never appear. They only appear at warn or above, and only once per debounce window per (window, status). Check claude-usage status for the current overall, and check that the hook groups are in ~/.claude/settings.json.

Sessions show discovered: "transcript". The daemon was down when they started, so they were back-filled from their transcripts and have no trusted pid. They can be soft-paused but not hard-frozen (409).

Development

npm ci
npm test          # vitest
npm run typecheck # tsc --noEmit
npm run lint      # eslint
npm run build     # tsc -p tsconfig.build.json → dist/

TDD per module; fixtures under test/fixtures/ contain no credentials and no real prompt text. lefthook runs lint + typecheck pre-commit (npm run prepare installs it). CI runs lint, typecheck, test and build on ubuntu-latest and macos-latest.

What CI cannot cover — launchd, systemd, the Keychain, a real Tailscale peer, a real SIGSTOPed process tree — is the manual checklist in docs/smoke-test.md.

Docs

  • docs/api.md — every HTTP endpoint, status code, error envelope, the SSE wire format, and the known deviations between spec and code.

  • docs/security.md — threat model, what the token protects, file modes, the escape hatch, update integrity, how to report an issue.

  • docs/smoke-test.md — the manual checklist.

  • CHANGELOG.md.


Works with Claude Code. Not affiliated with, endorsed by, or sponsored by Anthropic. Claude and Claude Code are trademarks of Anthropic.

MIT © contributors. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    Not graded
    maintenance
    Provides comprehensive telemetry and usage analytics for Claude Code sessions, including token usage tracking, cost monitoring, and tool usage patterns. Enables users to monitor their Claude usage with detailed metrics, warnings, and trend analysis.
    12
    -
  • A
    license
    A
    quality
    Not graded
    maintenance
    Provides real-time visibility into Claude Pro and Max subscription usage limits directly within Claude Code by utilizing local OAuth tokens. It enables users to monitor session and weekly usage across different models and receive alerts regarding rate-limiting status.
    4
    -
  • A
    license
    A
    quality
    D
    maintenance
    Wraps Claude Code as tools for MCP clients, enabling autonomous coding tasks via a 4-tool lifecycle with session management, async polling, and permission controls.
    4
    41 npm
    20
    MIT