Skip to content

Instantly share code, notes, and snippets.

@hamiltont
Created August 17, 2026 14:42
Show Gist options
  • Select an option

  • Save hamiltont/7edec59f3124ed5f00315fb086c84da4 to your computer and use it in GitHub Desktop.

Select an option

Save hamiltont/7edec59f3124ed5f00315fb086c84da4 to your computer and use it in GitHub Desktop.
Monitor your Claude subscription usage quota / quotas from Ralph loops (best effort, uses private API)
#!/usr/bin/env bash
# claude-usage — authoritative Claude subscription usage via the undocumented
# OAuth endpoint (the same data that powers Claude Code's `/usage` panel).
#
# Notes / gotchas:
# * Undocumented internal endpoint — fields can churn (tangelo, iguana_necktie,
# omelette_promotional, etc. are experimental). Treat automation as best-effort.
# * Sends the same UA as the official client; requests without it are rejected.
# * Do NOT poll faster than ~180s or you'll rate-limit yourself.
# * `curl -q` ignores ~/.curlrc (which appends a -w timing line that breaks JSON).
#
# Usage:
# claude-usage # pretty human summary
# claude-usage --json # raw JSON (for agents / piping to jq)
set -euo pipefail
raw=false
[ "${1:-}" = "--json" ] && raw=true
ver=$(claude --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
[ -z "$ver" ] && ver="0.0.0"
# Token: macOS Keychain first, then ~/.claude/.credentials.json, then env var.
token=""
if command -v security >/dev/null 2>&1 && \
security find-generic-password -s "Claude Code-credentials" -w >/dev/null 2>&1; then
token=$(security find-generic-password -s "Claude Code-credentials" -w \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["claudeAiOauth"]["accessToken"])')
elif [ -f "$HOME/.claude/.credentials.json" ]; then
token=$(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.claude/.credentials.json")))["claudeAiOauth"]["accessToken"])')
elif [ -n "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
token="$CLAUDE_CODE_OAUTH_TOKEN"
fi
if [ -z "$token" ]; then
echo "claude-usage: no OAuth token found (Keychain / ~/.claude/.credentials.json / \$CLAUDE_CODE_OAUTH_TOKEN)" >&2
exit 1
fi
resp=$(curl -qs --max-time 15 https://api.anthropic.com/api/oauth/usage \
-H "Authorization: Bearer $token" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "User-Agent: claude-code/$ver" \
-H "Content-Type: application/json")
if [ -z "$resp" ]; then
echo "claude-usage: empty response (rate-limited? token expired? run 'claude' once to refresh)" >&2
exit 1
fi
# An expired or invalid token is NOT a transport failure: the endpoint answers
# with a normal body carrying {"type":"error"}. That parses fine and simply has
# none of the window keys, so an unguarded render prints the bare header and
# exits 0 -- a total auth failure that reads as a clean run with no usage. Catch
# it before rendering.
err=$(CLAUDE_USAGE_RESP="$resp" python3 -c '
import os, json
try:
d = json.loads(os.environ["CLAUDE_USAGE_RESP"])
except ValueError:
print("unparseable response from the usage endpoint"); raise SystemExit
if isinstance(d, dict) and d.get("type") == "error":
print((d.get("error") or {}).get("message") or "unknown API error")
')
if [ -n "$err" ]; then
$raw && printf '%s\n' "$resp"
echo "claude-usage: $err" >&2
echo "claude-usage: run 'claude' once on this machine to refresh the OAuth token" >&2
exit 1
fi
if $raw; then
printf '%s\n' "$resp"
exit 0
fi
CLAUDE_USAGE_RESP="$resp" python3 <<'PY'
import os, json, sys
from datetime import datetime, timezone
d = json.loads(os.environ["CLAUDE_USAGE_RESP"])
rows = []
def pct(v):
# The endpoint returns an explicit null for utilization on windows it is
# not tracking, so a .get() default never fires. Render, don't crash.
return " -" if v is None else f"{v:>5.1f}"
def line(label, w):
if not w: return
u = w.get("utilization")
r = w.get("resets_at")
when = ""
if r:
try:
dt = datetime.fromisoformat(r.replace("Z","+00:00"))
rem = dt - datetime.now(timezone.utc)
secs = int(rem.total_seconds())
h, m = divmod(max(secs,0)//60, 60)
when = f" (resets in {h}h{m:02d}m — {dt.astimezone().strftime('%a %H:%M')})"
except Exception:
when = f" (resets {r})"
rows.append(f" {label:<16} {pct(u)}%{when}")
line("5-hour", d.get("five_hour"))
line("7-day (all)", d.get("seven_day"))
line("7-day Opus", d.get("seven_day_opus"))
line("7-day Sonnet",d.get("seven_day_sonnet"))
ex = d.get("extra_usage") or {}
sp = d.get("spend") or {}
if ex.get("is_enabled") or sp.get("enabled"):
# MONEY IS IN MINOR UNITS. The `spend` block says so unambiguously —
# {"amount_minor": 3000, "exponent": 2} is $30.00, not $3000. The
# `extra_usage` block carries the same numbers with the exponent named
# "decimal_places", which reads like display precision and is not: using it
# that way prints a 100x overstatement of the user's spending cap. Prefer
# `spend`, which is self-describing, and treat `extra_usage` as fallback.
def money(m):
if not isinstance(m, dict) or m.get("amount_minor") is None: return None
return m["amount_minor"] / (10 ** m.get("exponent", 2))
used = money(sp.get("used"))
lim = money(sp.get("limit"))
cur = (sp.get("limit") or {}).get("currency") or ex.get("currency", "")
if used is None or lim is None:
exp = ex.get("decimal_places", 2)
used = None if ex.get("used_credits") is None else ex["used_credits"] / (10 ** exp)
lim = None if ex.get("monthly_limit") is None else ex["monthly_limit"] / (10 ** exp)
u = sp.get("percent")
if u is None: u = ex.get("utilization")
# utilization is null until credits are actually spent; derive it.
if u is None and used is not None and lim:
u = used / lim * 100
amt = "?" if used is None or lim is None else f"{used:,.2f}/{lim:,.2f} {cur}".strip()
rows.append(f" {'extra usage':<16} {pct(u)}% ({amt})")
# Nothing to show means the payload carried none of the windows we know how to
# read. Report it as a failure rather than printing a lone header that looks
# like "you have used nothing".
if not rows:
print("claude-usage: response contained no recognizable usage windows", file=sys.stderr)
print("claude-usage: the endpoint's shape may have changed -- inspect with --json", file=sys.stderr)
raise SystemExit(1)
print("Claude subscription usage")
print("\n".join(rows))
PY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment