# Claude Usage Meter

> 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.

- Skill: `tankygranny05/claude-usage-meter` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add tankygranny05/claude-usage-meter`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tankygranny05/claude-usage-meter/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: tankygranny05 (https://skillmd.com/u/tankygranny05)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/tankygranny05/claude-usage-meter

---

<!-- [Edited by Claude: 74b7884b-c678-4264-bf72-363a827187bc 2026-01-24] -->

# Claude Usage Meter

Fetch Claude Code usage limits programmatically without the interactive `/usage` command.

## Quick Start

```bash
# 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
```

---

## Critical Knowledge (Lessons Learned)

### ⚠️ 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:

```json
// oauth_token_dump.json (RAW - has extra fields)
{
  "event": "oauth_token_dump",
  "ts": "2026-01-24T...",
  "claudeAiOauth": { ... }  // <-- Only this part is needed
}
```

```json
// .credentials.json (CORRECT format)
{
  "claudeAiOauth": {
    "accessToken": "...",
    "refreshToken": "...",
    "expiresAt": 1737812345678,
    "scopes": ["user:inference", "..."],
    "subscriptionType": "pro",
    "rateLimitTier": "..."
  }
}
```

### ⚠️ IMPORTANT: Use -p Flag to Avoid Blocking

When running Claude CLI for token refresh, you MUST use `-p` flag with a prompt:

```bash
# 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"
```

### ⚠️ 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:

```bash
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

```json
{
  "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)

```json
{
  "event": "oauth_token_dump",
  "ts": "ISO 8601 timestamp",
  "claudeAiOauth": {
    "accessToken": "...",
    "refreshToken": "...",
    "expiresAt": 1737812345678,
    "scopes": [...],
    "subscriptionType": "...",
    "rateLimitTier": "..."
  }
}
```

### Usage API Response Schema

```json
{
  "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

```http
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:

```bash
# 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:**
```bash
scp -P 22222 ~/.claude/.credentials.json user@host:~/.claude/.credentials.json
```

---

## Smoke Test Strategy

### Test 1: Token Refresh (Local)

```bash
# 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>"
```

### Test 2: Token Conversion

```bash
# 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')
"
```

### Test 3: Usage Fetch

```bash
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
```

### Test 4: Claude CLI Works

```bash
~/symlinks/claude -p "Respond with exactly: OAuth functional"

# Expected: "OAuth functional"
# If you see "Invalid API key": .credentials.json is missing or malformed
```

### Test 5: Remote Deployment

```bash
# 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"
```

### Test 6: Timeout Behavior

```bash
# 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
```

### Test 7: Error Handling - Missing Token

```bash
# 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/
```

---

## Inline Code Reference

### refresh_oauth.py

```python
#!/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()
```

### convert_oauth_to_credentials.py

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

### fetch_usage.py (Core Logic)

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

---

## Troubleshooting

### "Invalid API key - Please run /login"

**Cause**: `.credentials.json` is missing or malformed.

**Fix**:
```bash
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**:
```bash
ssh remote 'source ~/.zshrc; ~/symlinks/claude -p "test"'
```

### 401 Authentication Error on Usage API

**Cause**: Token expired or wrong format.

**Fix**: Refresh and convert:
```bash
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)

