#!/bin/bash # Claude Code Usage Checker # Queries Anthropic OAuth API for Claude Code rate limits set -euo pipefail CACHE_FILE="${CACHE_FILE:-/tmp/claude-usage-cache}" CACHE_TTL="${CACHE_TTL:-60}" # 1 minute default # Parse arguments FORCE_REFRESH=0 FORMAT="text" while [[ $# -gt 0 ]]; do case $1 in --fresh|--force) FORCE_REFRESH=1 shift ;; --json) FORMAT="json" shift ;; --cache-ttl) CACHE_TTL="$2" shift 2 ;; --help|-h) cat << 'EOF' Usage: claude-usage.sh [OPTIONS] Check Claude Code OAuth usage limits (session & weekly). Options: --fresh, --force Force refresh (ignore cache) --json Output as JSON --cache-ttl SEC Cache TTL in seconds (default: 60) --help, -h Show this help Examples: claude-usage.sh # Use cache if fresh claude-usage.sh --fresh # Force API call claude-usage.sh --json # JSON output EOF exit 0 ;; *) echo "Unknown option: $1" >&2 exit 1 ;; esac done # Function to convert seconds to human readable secs_to_human() { local secs=$1 if [ "$secs" -lt 0 ]; then secs=0; fi local days=$((secs / 86400)) local hours=$(((secs % 86400) / 3600)) local mins=$(((secs % 3600) / 60)) if [ "$days" -gt 0 ]; then echo "${days}d ${hours}h" elif [ "$hours" -gt 0 ]; then echo "${hours}h ${mins}m" else echo "${mins}m" fi } # Check cache (unless force refresh) if [ "$FORCE_REFRESH" -eq 0 ] && [ -f "$CACHE_FILE" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then age=$(($(date +%s) - $(stat -f%m "$CACHE_FILE"))) else age=$(($(date +%s) - $(stat -c%Y "$CACHE_FILE"))) fi if [ "$age" -lt "$CACHE_TTL" ]; then cat "$CACHE_FILE" exit 0 fi fi # Get OAuth token from keychain (macOS) if [[ "$OSTYPE" == "darwin"* ]]; then CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || echo "") else # Linux: check common credential stores if command -v secret-tool >/dev/null 2>&1; then CREDS=$(secret-tool lookup application "Claude Code" 2>/dev/null || echo "") else echo "Error: Credential storage not found (macOS keychain or secret-tool required)" >&2 exit 1 fi fi if [ -z "$CREDS" ]; then if [ "$FORMAT" = "json" ]; then echo '{"error":"no_credentials","session":null,"weekly":null}' else echo "❌ No Claude Code credentials found" fi exit 1 fi TOKEN=$(echo "$CREDS" | grep -o '"accessToken":"[^"]*"' | sed 's/"accessToken":"//;s/"//') REFRESH_TOKEN=$(echo "$CREDS" | grep -o '"refreshToken":"[^"]*"' | sed 's/"refreshToken":"//;s/"//') EXPIRES_AT=$(echo "$CREDS" | grep -o '"expiresAt":[0-9]*' | sed 's/"expiresAt"://') if [ -z "$TOKEN" ]; then if [ "$FORMAT" = "json" ]; then echo '{"error":"no_token","session":null,"weekly":null}' else echo "❌ Could not extract access token" fi exit 1 fi # Check if token is expired and refresh if needed if [ -n "$EXPIRES_AT" ]; then NOW_MS=$(($(date +%s) * 1000)) if [ "$NOW_MS" -gt "$EXPIRES_AT" ]; then # Token expired - trigger Claude CLI to auto-refresh if command -v claude >/dev/null 2>&1; then # Run a simple query to trigger token refresh echo "2+2" | claude >/dev/null 2>&1 || true # Reload credentials from keychain after refresh if [[ "$OSTYPE" == "darwin"* ]]; then CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || echo "") else if command -v secret-tool >/dev/null 2>&1; then CREDS=$(secret-tool lookup application "Claude Code" 2>/dev/null || echo "") fi fi if [ -n "$CREDS" ]; then TOKEN=$(echo "$CREDS" | grep -o '"accessToken":"[^"]*"' | sed 's/"accessToken":"//;s/"//') fi else if [ "$FORMAT" = "json" ]; then echo '{"error":"token_expired","session":null,"weekly":null}' else echo "❌ OAuth token expired. Run 'claude' CLI to refresh." fi exit 1 fi fi fi # Fetch usage from API RESP=$(curl -s "https://api.anthropic.com/api/oauth/usage" \ -H "Authorization: Bearer $TOKEN" \ -H "anthropic-beta: oauth-2025-04-20" 2>/dev/null) if [ -z "$RESP" ]; then if [ "$FORMAT" = "json" ]; then echo '{"error":"api_error","session":null,"weekly":null}' else echo "❌ API request failed" fi exit 1 fi # Parse session (5-hour) SESSION=$(echo "$RESP" | grep -o '"five_hour":{[^}]*}' | grep -o '"utilization":[0-9]*' | sed 's/.*://') SESSION_RESET=$(echo "$RESP" | grep -o '"five_hour":{[^}]*}' | grep -o '"resets_at":"[^"]*"' | sed 's/"resets_at":"//;s/"//') # Parse weekly (7-day) WEEKLY=$(echo "$RESP" | grep -o '"seven_day":{[^}]*}' | grep -o '"utilization":[0-9]*' | sed 's/.*://') WEEKLY_RESET=$(echo "$RESP" | grep -o '"seven_day":{[^}]*}' | grep -o '"resets_at":"[^"]*"' | sed 's/"resets_at":"//;s/"//') SESSION=${SESSION:-0} WEEKLY=${WEEKLY:-0} # Calculate time until reset NOW=$(date +%s) if [ -n "$SESSION_RESET" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then SESSION_TS=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${SESSION_RESET%Z}" +%s 2>/dev/null || echo 0) else SESSION_TS=$(date -d "${SESSION_RESET}" +%s 2>/dev/null || echo 0) fi SESSION_LEFT=$(secs_to_human $((SESSION_TS - NOW))) else SESSION_LEFT="unknown" fi if [ -n "$WEEKLY_RESET" ]; then if [[ "$OSTYPE" == "darwin"* ]]; then WEEKLY_TS=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${WEEKLY_RESET%Z}" +%s 2>/dev/null || echo 0) else WEEKLY_TS=$(date -d "${WEEKLY_RESET}" +%s 2>/dev/null || echo 0) fi WEEKLY_LEFT=$(secs_to_human $((WEEKLY_TS - NOW))) else WEEKLY_LEFT="unknown" fi # Output format if [ "$FORMAT" = "json" ]; then OUTPUT=$(cat < "$CACHE_FILE" echo "$OUTPUT"