#!/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) "