| name | claude-usage-meter |
| description | Programmatically fetch Claude Code usage limits and remaining quota via API. Use when user asks about usage, rate limits, quota, billing blocks, or remaining capacity. Also handles OAuth token refresh for API access. |
Claude Usage Meter
Fetch Claude Code usage limits programmatically without the interactive /usage command.
Quick Start
mkdir -p /tmp/agent-XXXXX
CLAUDE_CONFIG_DIR=/tmp/agent-XXXXX CLAUDE_CODE_ENABLE_OAUTH_TOKEN_DUMP=1 ~/swe/claude-code-2.0.28/cli.js
python scripts/convert_oauth_to_credentials.py --config-dir /tmp/agent-XXXXX --output ~/.claude/.credentials.json
python scripts/upload_credentials_to_remotes.py
python scripts/fetch_usage.py
Critical Knowledge (Lessons Learned)
⚠️ CRITICAL: Claude Code Version for OAuth Refresh
Version 2.0.28 is an OLD version mentioned in this skill for historical reference only.
AGENTS MUST:
- Check the user's CURRENT Claude Code version by reading
~/symlinks/claude
- Expect version 2.1.15 or NEWER depending on when you read this skill (as of 2026-01-25)
- Use the CURRENT version for OAuth refresh, NOT 2.0.28
- Verify the current version supports
CLAUDE_CODE_ENABLE_OAUTH_TOKEN_DUMP before running
Example workflow:
ls -la ~/symlinks/claude
export CLAUDE_CODE_2028_PATH=~/swe/claude-code-2.1.15/cli.js
python scripts/refresh_oauth.py
Why 2.0.28 appears in this skill: A previous agent documented this skill when 2.0.28 was the OAuth-capable version. The skill name CLAUDE_CODE_2028_PATH is now a misnomer - it should point to your CURRENT Claude Code version.
⚠️ IMPORTANT: File Naming Matters
Claude Code reads from .credentials.json (WITH leading dot), NOT oauth_token_dump.json.
| File | Purpose | Read by Claude Code? |
|---|
.credentials.json | Active credentials | ✅ YES |
oauth_token_dump.json | Raw dump from 2.0.28 | ❌ NO (intermediate only) |
Common mistake: Copying oauth_token_dump.json to remote and expecting it to work. It won't.
⚠️ IMPORTANT: Token Dump Has Extra Fields
The oauth_token_dump.json contains extra envelope fields that must be stripped:
{
"event": "oauth_token_dump",
"ts": "2026-01-24T...",
"claudeAiOauth": { ... }
}
{
"claudeAiOauth": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1737812345678,
"scopes": ["user:inference", "..."],
"subscriptionType": "pro",
"rateLimitTier": "..."
}
}
⚠️ IMPORTANT: OAuth Login is Interactive TUI
CRITICAL: OAuth authentication requires interactive TUI - you CANNOT use -p flag for initial login.
~/swe/claude-code-2.0.28/cli.js -p "OAuth refresh ping"
CLAUDE_CONFIG_DIR=/tmp/agent-XXXXX CLAUDE_CODE_ENABLE_OAUTH_TOKEN_DUMP=1 ~/swe/claude-code-2.0.28/cli.js
Once you have a valid token, THEN you can use -p for subsequent non-interactive calls.
⚠️ IMPORTANT: Always Read Scripts Before Running
The refresh script already has -p built in. Read it first before blindly passing flags.
⚠️ IMPORTANT: Never Log/Print OAuth Tokens
When agents handle OAuth tokens:
- NEVER read token files and print contents
- NEVER cat/echo token files
- Use scripts that process tokens without exposing them
- Tokens in conversation logs = security risk
⚠️ IMPORTANT: Backup Before Refresh
Always backup existing token before refreshing:
mkdir -p ~/temp/backups
mv ~/.claude/oauth_token_dump.json ~/temp/backups/oauth_token_dump_$(date +%Y%m%d_%H%M%S).json
File Schemas
.credentials.json Schema
{
"claudeAiOauth": {
"accessToken": "string (JWT)",
"refreshToken": "string (JWT)",
"expiresAt": "number (Unix ms timestamp)",
"scopes": ["user:inference", "..."],
"subscriptionType": "pro | free | ...",
"rateLimitTier": "string | null"
}
}
oauth_token_dump.json Schema (Raw Output)
{
"event": "oauth_token_dump",
"ts": "ISO 8601 timestamp",
"claudeAiOauth": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1737812345678,
"scopes": [...],
"subscriptionType": "...",
"rateLimitTier": "..."
}
}
Usage API Response Schema
{
"five_hour": {
"utilization": 8.0,
"resets_at": "2026-01-03T09:00:00Z"
},
"seven_day": {
"utilization": 7.0,
"resets_at": "2026-01-08T08:00:00Z"
},
"seven_day_opus": null,
"seven_day_sonnet": {
"utilization": 1.0,
"resets_at": "2026-01-08T19:00:00Z"
},
"extra_usage": {
"is_enabled": false,
"monthly_limit": null
}
}
API Details
Endpoint
GET https://api.anthropic.com/api/oauth/usage
Headers
Authorization: Bearer <accessToken>
Content-Type: application/json
User-Agent: claude-code/2.0.76
anthropic-beta: oauth-2025-04-20
Remote Deployment Workflow
To deploy OAuth credentials to a remote machine:
ssh remote 'mv ~/.claude/.credentials.json ~/temp/backup_creds_$(date +%Y%m%d).json 2>/dev/null || true'
python ~/.claude/skills/claude-usage-meter/scripts/refresh_oauth.py
python ~/.claude/skills/claude-usage-meter/scripts/convert_oauth_to_credentials.py
scp ~/.claude/.credentials.json remote:~/.claude/.credentials.json
ssh remote '~/symlinks/claude -p "Hello, respond: OAuth working"'
For SSH with custom port:
scp -P 22222 ~/.claude/.credentials.json user@host:~/.claude/.credentials.json
Quick Upload Command (Assumes credentials already in ~/.claude/.credentials.json)
Python script (recommended - handles SSH aliases correctly):
python ~/.claude/skills/claude-usage-meter/scripts/upload_credentials_to_remotes.py
python ~/.claude/skills/claude-usage-meter/scripts/upload_credentials_to_remotes.py cm3u cm2 other-alias
Shell one-liner (works if aliases are defined):
for alias in cm3u cm2; do cat ~/.claude/.credentials.json | $alias 'cat > ~/.claude/.credentials.json && chmod 600 ~/.claude/.credentials.json && echo "✅ Uploaded to '$alias'"'; done
Smoke Test Strategy
Test 1: Token Refresh (Local)
mv ~/.claude/oauth_token_dump.json ~/temp/backup_$(date +%s).json 2>/dev/null || true
python ~/.claude/skills/claude-usage-meter/scripts/refresh_oauth.py
ls -la ~/.claude/oauth_token_dump.json
Test 2: Token Conversion
python ~/.claude/skills/claude-usage-meter/scripts/convert_oauth_to_credentials.py
ls -la ~/.claude/.credentials.json
python -c "
import json
with open('$HOME/.claude/.credentials.json') as f:
d = json.load(f)
assert 'claudeAiOauth' in d, 'Missing claudeAiOauth key'
assert 'accessToken' in d['claudeAiOauth'], 'Missing accessToken'
assert 'refreshToken' in d['claudeAiOauth'], 'Missing refreshToken'
assert 'expiresAt' in d['claudeAiOauth'], 'Missing expiresAt'
print('✅ Structure valid')
"
Test 3: Usage Fetch
python ~/.claude/skills/claude-usage-meter/scripts/fetch_usage.py
Test 4: Claude CLI Works
~/symlinks/claude -p "Respond with exactly: OAuth functional"
Test 5: Remote Deployment
scp ~/.claude/.credentials.json remote:~/.claude/.credentials.json
ssh remote '~/symlinks/claude -p "Respond: Remote OAuth OK"'
Test 6: Timeout Behavior
timeout 30 python ~/.claude/skills/claude-usage-meter/scripts/refresh_oauth.py
echo "Exit code: $?"
Test 7: Error Handling - Missing Token
mv ~/.claude/oauth_token_dump.json /tmp/
python ~/.claude/skills/claude-usage-meter/scripts/convert_oauth_to_credentials.py
mv /tmp/oauth_token_dump.json ~/.claude/
Inline Code Reference
refresh_oauth.py
"""
Refresh OAuth token by running Claude Code 2.0.28 with dump flag.
Auto-kills after 20 seconds maximum.
"""
import os
import subprocess
import sys
import json
from datetime import datetime
from pathlib import Path
CLAUDE_CLI = os.environ.get(
"CLAUDE_CODE_2028_PATH",
os.path.expanduser("~/swe/claude-code-2.0.28/cli.js")
)
CONFIG_DIR = os.environ.get(
"CLAUDE_CONFIG_DIR",
os.path.expanduser("~/.claude")
)
TIMEOUT_SECONDS = 20
def main():
cli_path = Path(CLAUDE_CLI)
config_path = Path(CONFIG_DIR)
token_file = config_path / "oauth_token_dump.json"
if not cli_path.exists():
print(f"ERROR: Claude Code 2.0.28 not found at {cli_path}")
sys.exit(1)
print(f"Refreshing OAuth token...")
print(f"Config dir: {config_path}")
print(f"Timeout: {TIMEOUT_SECONDS}s")
env = os.environ.copy()
env["CLAUDE_CODE_ENABLE_OAUTH_TOKEN_DUMP"] = "1"
env["CLAUDE_CONFIG_DIR"] = str(config_path)
proc = subprocess.Popen(
[str(cli_path), "-p", "OAuth refresh ping"],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
try:
stdout, _ = proc.communicate(timeout=TIMEOUT_SECONDS)
for line in stdout.strip().split("\n")[:5]:
print(line)
except subprocess.TimeoutExpired:
print(f"Timed out after {TIMEOUT_SECONDS}s, killing...")
proc.kill()
proc.wait()
if token_file.exists():
with open(token_file) as f:
data = json.load(f)
expires_ms = data.get("claudeAiOauth", {}).get("expiresAt")
if expires_ms:
print(f"Token expires: {datetime.fromtimestamp(expires_ms / 1000)}")
else:
print("WARNING: Token file not created")
sys.exit(1)
if __name__ == "__main__":
main()
convert_oauth_to_credentials.py
"""
Convert oauth_token_dump.json to .credentials.json format.
Does not print or log any sensitive token data.
"""
import json
import os
import sys
from pathlib import Path
DEFAULT_CONFIG_DIR = os.path.expanduser("~/.claude")
def convert_oauth_to_credentials(config_dir=None, output_path=None):
config_dir = Path(config_dir or DEFAULT_CONFIG_DIR)
input_file = config_dir / "oauth_token_dump.json"
output_file = Path(output_path) if output_path else config_dir / ".credentials.json"
if not input_file.exists():
print(f"ERROR: {input_file} not found")
sys.exit(1)
with open(input_file) as f:
data = json.load(f)
if "claudeAiOauth" not in data:
print(f"ERROR: No 'claudeAiOauth' key in {input_file}")
sys.exit(1)
credentials = {"claudeAiOauth": data["claudeAiOauth"]}
output_file.parent.mkdir(parents=True, exist_ok=True)
with open(output_file, "w") as f:
json.dump(credentials, f, indent=2)
os.chmod(output_file, 0o600)
print(f"Created: {output_file}")
print(f"Permissions: 600")
expires_ms = data["claudeAiOauth"].get("expiresAt")
if expires_ms:
from datetime import datetime
print(f"Token expires: {datetime.fromtimestamp(expires_ms / 1000)}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--config-dir", "-c",
default=os.environ.get("CLAUDE_CONFIG_DIR", DEFAULT_CONFIG_DIR))
parser.add_argument("--output", "-o")
args = parser.parse_args()
convert_oauth_to_credentials(args.config_dir, args.output)
fetch_usage.py (Core Logic)
"""Fetch Claude Code usage limits via direct API call."""
import json
import sys
from pathlib import Path
import requests
OAUTH_TOKEN_PATHS = [
Path.home() / ".claude" / ".credentials.json",
Path.home() / ".claude" / "oauth_token_dump.json",
]
USAGE_ENDPOINT = "https://api.anthropic.com/api/oauth/usage"
def load_auth_token():
for path in OAUTH_TOKEN_PATHS:
if path.exists():
with open(path) as f:
data = json.load(f)
oauth = data.get("claudeAiOauth", {})
token = oauth.get("accessToken")
if token:
return token, oauth.get("subscriptionType", "unknown")
print("ERROR: No OAuth token found")
sys.exit(1)
def fetch_usage(token):
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"anthropic-beta": "oauth-2025-04-20"
}
resp = requests.get(USAGE_ENDPOINT, headers=headers, timeout=10)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
token, sub_type = load_auth_token()
usage = fetch_usage(token)
print(json.dumps(usage, indent=2))
Troubleshooting
"Invalid API key - Please run /login"
Cause: .credentials.json is missing or malformed.
Fix:
python scripts/refresh_oauth.py
python scripts/convert_oauth_to_credentials.py
Token refresh hangs forever
Cause: Missing -p flag or interactive mode.
Fix: The script handles this with 20s timeout. If still hanging, kill and check CLI path.
"env: node: No such file or directory" (Remote)
Cause: SSH non-interactive shell doesn't load PATH.
Fix:
ssh remote 'source ~/.zshrc; ~/symlinks/claude -p "test"'
401 Authentication Error on Usage API
Cause: Token expired or wrong format.
Fix: Refresh and convert:
python scripts/refresh_oauth.py
python scripts/convert_oauth_to_credentials.py
Token file not created after refresh
Cause: Claude Code version doesn't support CLAUDE_CODE_ENABLE_OAUTH_TOKEN_DUMP.
Fix: Must use Claude Code 2.0.28 specifically. Set CLAUDE_CODE_2028_PATH env var.
Environment Variables
| Variable | Default | Purpose |
|---|
CLAUDE_CONFIG_DIR | ~/.claude | Where to read/write credentials |
CLAUDE_CODE_2028_PATH | ~/swe/claude-code-2.0.28/cli.js | Path to Claude 2.0.28 for token dump |
Dependencies
- Python 3.8+
requests library (pip install requests)
- Claude Code 2.0.28 (for token refresh only)