Skip to content

Instantly share code, notes, and snippets.

@turlockmike
Created March 6, 2026 17:07
Show Gist options
  • Select an option

  • Save turlockmike/d353bf157bcc98cdbf9b147e10341e30 to your computer and use it in GitHub Desktop.

Select an option

Save turlockmike/d353bf157bcc98cdbf9b147e10341e30 to your computer and use it in GitHub Desktop.
Claude Code Usage Monitor — query rate limit status from the terminal (NDJSON output)

Claude Code Usage Monitor

Query your Claude Code rate limit status from the terminal. Returns NDJSON with utilization percentages and reset countdowns for each rate window.

Prerequisites

  • Claude Code installed and authenticated (claude auth)
  • macOS (reads OAuth token from Keychain via security CLI)
  • python3 and curl on PATH

Quick Start

chmod +x claude-usage.sh
./claude-usage.sh

Output (one JSON object per line):

{"window": "5h", "utilization": 42.3, "remaining": 57.7, "resets_at": "2026-03-06T18:00:00Z", "resets_in": "2h 14m"}
{"window": "7d", "utilization": 15.1, "remaining": 84.9, "resets_at": "2026-03-10T00:00:00Z", "resets_in": "3d 6h"}

Rate Windows

Window Description
5h 5-hour rolling window
7d 7-day aggregate
7d_opus 7-day Opus-specific budget
7d_sonnet 7-day Sonnet-specific budget
extra Overflow usage (only if enabled on your plan)

Integration Examples

Wrap in an API route (Next.js)

import { execFile } from 'child_process';
import { promisify } from 'util';
import { NextResponse } from 'next/server';

const exec = promisify(execFile);

export async function GET() {
  const { stdout } = await exec('./claude-usage.sh', [], { timeout: 30000 });
  const windows = stdout.trim().split('\n').filter(Boolean).map(JSON.parse);
  return NextResponse.json({ windows });
}

Poll from a React component

const [usage, setUsage] = useState([]);

useEffect(() => {
  const poll = async () => {
    const res = await fetch('/api/claude-usage');
    const { windows } = await res.json();
    setUsage(windows);
  };
  poll();
  const id = setInterval(poll, 15 * 60 * 1000); // every 15 min
  return () => clearInterval(id);
}, []);

One-liner: check if you're rate-limited

./claude-usage.sh | python3 -c "
import sys, json
for line in sys.stdin:
    w = json.loads(line)
    if w['utilization'] >= 80:
        print(f\"WARNING: {w['window']} at {w['utilization']}% — resets in {w['resets_in']}\")
"

How It Works

  1. Reads your Claude Code OAuth token from the macOS Keychain (Claude Code-credentials)
  2. Calls https://api.anthropic.com/api/oauth/usage with that token
  3. Parses the response into per-window NDJSON records with utilization and reset info
  4. Retries on 429 (rate limited) up to 3 times with exponential backoff
#!/bin/bash
# claude-usage — Query Claude Code rate limit usage from the OAuth API
# Output: NDJSON to stdout, errors to stderr, exit 0/1/2
#
# Prerequisites:
# - Claude Code installed and authenticated (the OAuth token lives in your OS keychain)
# - macOS (uses `security` CLI for keychain access)
# - python3 available on PATH
# - curl available on PATH
#
# Usage:
# ./claude-usage.sh # NDJSON output
# ./claude-usage.sh | python3 -m json.tool # pretty-print
#
# Output format (one JSON object per line):
# {"window":"5h","utilization":42.3,"remaining":57.7,"resets_at":"2026-03-06T18:00:00Z","resets_in":"2h 14m"}
# {"window":"7d","utilization":15.1,"remaining":84.9,"resets_at":"2026-03-10T00:00:00Z","resets_in":"3d 6h"}
#
# Windows emitted (when available):
# 5h — 5-hour rolling window
# 7d — 7-day rolling window (aggregate)
# 7d_opus — 7-day Opus-specific budget
# 7d_sonnet — 7-day Sonnet-specific budget
# extra — extra/overflow usage (only if enabled on your plan)
set -euo pipefail
# --- 1. Read OAuth token from macOS keychain ---
TOKEN=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null | python3 -c "
import sys, json
data = json.loads(sys.stdin.read().strip())
print(data['claudeAiOauth']['accessToken'])
" 2>/dev/null) || {
echo '{"error":"Failed to read OAuth token from keychain. Is Claude Code installed and authenticated?"}' >&2
exit 1
}
# --- 2. Call the usage API (with retry on 429) ---
RESP_FILE=$(mktemp)
trap 'rm -f "$RESP_FILE"' EXIT
for attempt in 1 2 3; do
HTTP_CODE=$(curl -s -o "$RESP_FILE" -w '%{http_code}' \
"https://api.anthropic.com/api/oauth/usage" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "User-Agent: claude-code/2.1.5" \
-H "anthropic-beta: oauth-2025-04-20" 2>/dev/null)
if [[ "$HTTP_CODE" == "200" ]]; then
RESPONSE=$(<"$RESP_FILE")
break
elif [[ "$HTTP_CODE" == "429" && "$attempt" -lt 3 ]]; then
sleep $((attempt * 2))
continue
elif [[ "$HTTP_CODE" == "401" || "$HTTP_CODE" == "403" ]]; then
echo '{"error":"Auth failed. Token may be expired — run: claude auth"}' >&2
exit 1
else
echo "{\"error\":\"API returned HTTP $HTTP_CODE\"}" >&2
exit 1
fi
done
# --- 3. Parse response into NDJSON ---
echo "$RESPONSE" | python3 -c "
import sys, json
from datetime import datetime, timezone
data = json.loads(sys.stdin.read())
def fmt_reset(iso_str):
dt = datetime.fromisoformat(iso_str)
now = datetime.now(timezone.utc)
delta = dt - now
total_min = int(delta.total_seconds() / 60)
if total_min < 0:
return '0m'
h, m = divmod(total_min, 60)
return f'{h}h {m}m' if h else f'{m}m'
def emit(window, info):
if info is None:
return
record = {
'window': window,
'utilization': info['utilization'],
'remaining': round(100 - info['utilization'], 1),
'resets_at': info['resets_at'],
'resets_in': fmt_reset(info['resets_at'])
}
print(json.dumps(record))
emit('5h', data.get('five_hour'))
emit('7d', data.get('seven_day'))
emit('7d_opus', data.get('seven_day_opus'))
emit('7d_sonnet', data.get('seven_day_sonnet'))
emit('extra', data.get('extra_usage') if data.get('extra_usage', {}).get('is_enabled') else None)
"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment