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...
Fetch Claude Code usage limits programmatically without the interactive /usage command.
# 1. Refresh token (generates oauth_token_dump.json)
python scripts/refresh_oauth.py
# 2. Convert to .credentials.json
python scripts/convert_oauth_to_credentials.py
# 3. Fetch usage
python scripts/fetch_usage.py
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.
The oauth_token_dump.json contains extra envelope fields that must be stripped:
// oauth_token_dump.json (RAW - has extra fields)
{
"event": "oauth_token_dump",
"ts": "2026-01-24T...",
"claudeAiOauth": { ... } // <-- Only this part is needed
}
// .credentials.json (CORRECT format)
{
"claudeAiOauth": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1737812345678,
"scopes": ["user:inference", "..."],
"subscriptionType": "pro",
"rateLimitTier": "..."
}
}
When running Claude CLI for token refresh, you MUST use -p flag with a prompt:
# WRONG - blocks forever waiting for input
~/swe/claude-code-2.0.28/cli.js
# CORRECT - exits after responding
~/swe/claude-code-2.0.28/cli.js -p "OAuth refresh ping"
The refresh script already has -p built in. Read it first before blindly passing flags.
When agents handle OAuth tokens:
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
{
"claudeAiOauth": {
"accessToken": "string (JWT)",
"refreshToken": "string (JWT)",
"expiresAt": "number (Unix ms timestamp)",
"scopes": ["user:inference", "..."],
"subscriptionType": "pro | free | ...",
"rateLimitTier": "string | null"
}
}
{
"event": "oauth_token_dump",
"ts": "ISO 8601 timestamp",
"claudeAiOauth": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1737812345678,
"scopes": [...],
"subscriptionType": "...",
"rateLimitTier": "..."
}
}
{
"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
}
}
GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer <accessToken>
Content-Type: application/json
User-Agent: claude-code/2.0.76
anthropic-beta: oauth-2025-04-20
To deploy OAuth credentials to a remote machine:
# 1. Backup existing remote token (if any)
ssh remote 'mv ~/.claude/.credentials.json ~/temp/backup_creds_$(date +%Y%m%d).json 2>/dev/null || true'
# 2. Refresh local token
python ~/.claude/skills/claude-usage-meter/scripts/refresh_oauth.py
# 3. Convert to credentials format
python ~/.claude/skills/claude-usage-meter/scripts/convert_oauth_to_credentials.py
# 4. Copy to remote (tokens never seen by agent)
scp ~/.claude/.credentials.json remote:~/.claude/.credentials.json
# 5. Test on remote
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
# Backup existing
mv ~/.claude/oauth_token_dump.json ~/temp/backup_$(date +%s).json 2>/dev/null || true
# Run refresh
python ~/.claude/skills/claude-usage-meter/scripts/refresh_oauth.py
# Verify output file exists
ls -la ~/.claude/oauth_token_dump.json
# Expected: File exists, created within last minute
# Expected output: "Token expires: <future datetime>"
# Run conversion
python ~/.claude/skills/claude-usage-meter/scripts/convert_oauth_to_credentials.py
# Verify .credentials.json exists with correct permissions
ls -la ~/.claude/.credentials.json
# Expected: -rw------- (600 permissions)
# Verify structure without exposing token
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')
"
python ~/.claude/skills/claude-usage-meter/scripts/fetch_usage.py
# Expected output includes:
# - "Using token from: ..."
# - "5-Hour Block:" with utilization percentage
# - "Raw API Response:" with JSON
~/symlinks/claude -p "Respond with exactly: OAuth functional"
# Expected: "OAuth functional"
# If you see "Invalid API key": .credentials.json is missing or malformed
# Copy credentials
scp ~/.claude/.credentials.json remote:~/.claude/.credentials.json
# Test remote
ssh remote '~/symlinks/claude -p "Respond: Remote OAuth OK"'
# Expected: "Remote OAuth OK"
# The refresh script should auto-kill after 20s
# Test by checking process doesn't hang
timeout 30 python ~/.claude/skills/claude-usage-meter/scripts/refresh_oauth.py
echo "Exit code: $?"
# Expected: Completes within 30s, exit code 0
# Temporarily move token
mv ~/.claude/oauth_token_dump.json /tmp/
# Should fail gracefully
python ~/.claude/skills/claude-usage-meter/scripts/convert_oauth_to_credentials.py
# Expected: "ERROR: ... not found"
# Restore
mv /tmp/oauth_token_dump.json ~/.claude/
#!/usr/bin/env python
"""
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()
#!/usr/bin/env python
"""
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)
# Extract only claudeAiOauth (strips event, ts fields)
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)
#!/usr/bin/env python
"""Fetch Claude Code usage limits via direct API call."""
import json
import sys
from pathlib import Path
import requests # pip install 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))
Cause: .credentials.json is missing or malformed.
Fix:
python scripts/refresh_oauth.py
python scripts/convert_oauth_to_credentials.py
Cause: Missing -p flag or interactive mode.
Fix: The script handles this with 20s timeout. If still hanging, kill and check CLI path.
Cause: SSH non-interactive shell doesn't load PATH.
Fix:
ssh remote 'source ~/.zshrc; ~/symlinks/claude -p "test"'
Cause: Token expired or wrong format.
Fix: Refresh and convert:
python scripts/refresh_oauth.py
python scripts/convert_oauth_to_credentials.py
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.
| 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 |
requests library (pip install requests)