Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save jtbr/4f99671d1cee06b44106456958caba8b to your computer and use it in GitHub Desktop.

Select an option

Save jtbr/4f99671d1cee06b44106456958caba8b to your computer and use it in GitHub Desktop.
Claude Code Status Line: Usage Limits, Pacing Targets, and Context Window - Complete guide with all the gotchas

Claude Code Status Line: Usage Limits, Pacing Targets, and Context Window

A complete guide to building a Claude Code status line that shows your 5-hour and weekly usage limits with color-coded progress bars, pacing markers, reset times, and context window usage.

What you get:

yearone-3 main* │ Opus 4.6 │ 07:00 PM │ ctx ▓▓░░░░░░░░ 20% │ 5hr (11pm) ▓▓▓│░░░░░░ 37% │ wk (thu, 10am) ▓▓░│░░░░░░ 26%
  • Color-coded bars: green (<50%), yellow (50-80%), bright red (>80%)
  • Hot pink pacing marker showing where you should be for even usage across the window
  • Reset times so you know when your limits refill

Prerequisites

  • macOS (uses security keychain CLI and BSD date) (linux uses a plain file, no requirements)
  • jq installed (brew install jq)
  • Claude Code with an active subscription (Max, Pro, etc.)

Setup

Step 1: Create the status line script

Save the file below as ~/.claude/statusline-command.sh:

Step 2: Make it executable

chmod +x ~/.claude/statusline-command.sh

Step 3: Configure Claude Code

Add this to ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "bash ~/.claude/statusline-command.sh"
  }
}

Step 4: Ensure your keychain has a valid token

macOS

# Check if you have a valid token
security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null | jq '.claudeAiOauth.scopes'

You need ["user:inference", "user:profile"]. If the entry is missing, expired, or has wrong scopes:

# Delete stale entry
security delete-generic-password -s "Claude Code-credentials"

# Quit ALL Claude Code instances, then restart.
# CC opens a browser for OAuth — this creates a fresh keychain entry with correct scopes.

Linux

Ensure that ~/.claude/.credentials.json is populated

Step 5: Verify

# Test the script manually
echo '{"model":{"display_name":"Test"},"workspace":{"current_dir":"/tmp"},"context_window":{"used_percentage":42}}' \
  | bash ~/.claude/statusline-command.sh

Customization

Colors

The script uses ANSI 256-color codes. Change these to taste:

Element Current Code
Directory Light cyan \033[96m
Pacing marker Hot pink \033[38;5;199m
Good (<50%) Green \033[32m
Warning (50-80%) Yellow \033[33m
Danger (>80%) Bright red \033[91m

Dark red (\033[31m) is nearly invisible on dark terminal backgrounds. Use bright red (\033[91m) instead.

Bar width

Add a width argument to make_bar() to make bars wider or narrower than 10 characters

Cache interval

Change USAGE_CACHE_AGE=60 to control how often the API is called (in seconds).

Available status line JSON fields

Claude Code pipes these fields to your script via stdin:

Field Description
model.display_name Current model name
model.id Model identifier
context_window.used_percentage Context window usage
context_window.total_input_tokens Cumulative input tokens
context_window.total_output_tokens Cumulative output tokens
cost.total_cost_usd Session cost
cost.total_duration_ms Total session time
cost.total_lines_added Lines added
cost.total_lines_removed Lines removed
workspace.current_dir Current directory
output_style.name Output style
vim.mode Vim mode (if enabled)

Troubleshooting

Quick diagnostic

Run this to check everything at once:

#!/usr/bin/bash

echo "=== 1. jq installed? ==="
which jq && jq --version || echo "MISSING: brew install jq"

echo -e "\n=== 2. Credentials found? ==="
CREDS=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) # macos
[ -z "$CREDS" ] && CREDS=$(<~/.claude/.credentials.json) # linux, wsl
if [ -z "$CREDS" ]; then
    echo "MISSING: No keychain entry or credentials file. Quit all CC instances and restart to trigger OAuth login."
else
    echo "Found keychain entry"
fi

echo -e "\n=== 3. Has claudeAiOauth? ==="
echo "$CREDS" | jq -e '.claudeAiOauth' >/dev/null 2>&1 \
    && echo "YES" \
    || echo "NO — keychain only has: $(echo "$CREDS" | jq -r 'keys | join(", ")'). Delete entry and restart CC."

echo -e "\n=== 4. Token scopes ==="
echo "$CREDS" | jq -r '.claudeAiOauth.scopes // ["none"] | join(", ")' 2>/dev/null
echo "(need: user:inference, user:profile)"

echo -e "\n=== 5. Token expired? ==="
EXPIRES=$(echo "$CREDS" | jq -r '.claudeAiOauth.expiresAt // 0' 2>/dev/null)
NOW_MS=$(($(date +%s) * 1000))
echo $EXPIRES
if [ "$EXPIRES" -gt "$NOW_MS" ] 2>/dev/null; then
    echo "VALID — expires $(date -d @$((EXPIRES / 1000)))"
else
    echo "EXPIRED — delete keychain entry and restart CC"
fi

echo -e "\n=== 6. API test ==="
TOKEN=$(echo "$CREDS" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
if [ -n "$TOKEN" ]; then
    RESP=$(curl -s --max-time 5 "https://api.anthropic.com/api/oauth/usage" \
        -H "Authorization: Bearer $TOKEN" \
        -H "anthropic-beta: oauth-2025-04-20" \
        -H "Content-Type: application/json")
    if echo "$RESP" | jq -e '.five_hour' >/dev/null 2>&1; then
        echo "SUCCESS"
        echo "$RESP" | jq '{five_hour: .five_hour.utilization, seven_day: .seven_day.utilization}'
    else
        echo "FAILED: $(echo "$RESP" | jq -r '.error.message // "unknown error"')"
    fi
else
    echo "SKIPPED — no token"
fi

echo -e "\n=== 7. Script test ==="
echo '{"model":{"display_name":"Test"},"workspace":{"current_dir":"/tmp"},"context_window":{"used_percentage":42}}' \
    | bash ./statusline-command.sh 2>&1 && echo -e "\n(exit: 0)" || echo -e "\n(exit: $?)"

Common issues

Problem Cause Fix
No status line at all Script crashes with non-zero exit Run the diagnostic above — step 7 shows the error
No usage data (only ctx shows) Keychain token expired or missing Run diagnostic steps 2-6, then delete entry + restart CC
Usage shows 0% with bar at far left Stale cache from a previous window rm /tmp/claude-statusline-usage.json to force a fresh fetch
Pacing marker at far left (wrong) UTC timezone not handled in date parsing Ensure -u flag: date -juf not date -jf
printf: invalid format character ANSI escape codes in printf variable Use echo -e for final output, not printf
Token scope error (user:profile) Used claude setup-token That token only has user:inference. Delete keychain entry, restart CC for browser OAuth
Keychain has only mcpOAuth key Ran /login inside CC /login is for MCP servers, not CC auth. Delete entry, restart CC
Status line wraps to next line Output too wide for terminal Shorten labels (wk not weekly), drop user@host, drop seconds from time
jq: command not found jq not installed brew install jq
Keychain access popup/prompt macOS asking permission Click "Always Allow" — the script reads keychain on every cache refresh
Usage data is stale / not updating Cache file not refreshing Check ls -la /tmp/claude-statusline-usage.json — if mod time is old, token is probably expired
Script works manually but not in CC disableAllHooks: true in settings Remove that setting from ~/.claude/settings.json
Git branch slow in large repos git diff and git ls-files scanning Cache git info to a file with a 5-second TTL

The nuclear option

If nothing works, start completely fresh:

# 1. Delete keychain entry (MacOS only)
security delete-generic-password -s "Claude Code-credentials" 2>/dev/null

# 2. Delete stale cache
rm -f /tmp/claude-statusline-usage.json

# 3. Quit ALL Claude Code instances (all terminals)
# Cmd+Q or `killall claude` if desperate

# 4. Restart Claude Code
# It will open a browser for OAuth login
# This creates a fresh keychain entry with user:inference + user:profile scopes

# 5. Verify (MacOS only)
security find-generic-password -s "Claude Code-credentials" -w | jq '.claudeAiOauth.scopes'
# Should output: ["user:inference", "user:profile"]

Notes: Getting a Valid OAuth Token

The status line calls an undocumented Anthropic API (/api/oauth/usage) that requires an OAuth token with user:profile scope. Here's where it gets tricky.

How Claude Code stores credentials

Linux / WSL

Claude Code stores its OAuth credentials in a plain text file: ~/.claude/.credentials.json. Easy.

MacOS

Claude Code stores OAuth credentials in the macOS Keychain under the service name Claude Code-credentials. The entry contains JSON with a claudeAiOauth object that includes accessToken, refreshToken, expiresAt, and scopes.

Notes: Querying the Claude Usage API

Endpoint: GET https://api.anthropic.com/api/oauth/usage

Headers:

Authorization: Bearer <oauth_access_token>
anthropic-beta: oauth-2025-04-20
Content-Type: application/json

Response:

{
  "five_hour": {
    "utilization": 37.0,
    "resets_at": "2026-02-08T04:59:59.000000+00:00"
  },
  "seven_day": {
    "utilization": 26.0,
    "resets_at": "2026-02-12T14:59:59.771647+00:00"
  },
  "seven_day_opus": null,
  "seven_day_sonnet": {
    "utilization": 1.0,
    "resets_at": "2026-02-13T20:59:59.771655+00:00"
  },
  "extra_usage": {
    "is_enabled": false,
    "monthly_limit": null,
    "used_credits": null,
    "utilization": null
  }
}
  • utilization is a percentage (0-100)
  • resets_at is when the window ends (UTC)
  • five_hour is a rolling 5-hour window
  • seven_day is a rolling 7-day window

Credits

  • Usage API endpoint discovered via codelynx.dev
  • OAuth client ID and refresh flow reverse-engineered from the Claude Code binary
  • Context window calculation inspired by Richard-Weiss/ffadccca6238
#!/bin/bash
# Read Claude Code context from stdin
input=$(cat)
# Extract information from Claude Code context
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
current_dir=$(echo "$input" | jq -r '.workspace.current_dir // ""')
output_style=$(echo "$input" | jq -r '.output_style.name // "default"')
context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 10' | cut -d. -f1)
session_cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
session_cost="$(printf '$%.1f' $session_cost)"
# detect linux: [[ "$OSTYPE" == "linux-gnu"* ]]
# detect macos: [[ "$OSTYPE" == "darwin"* ]]
# Get username and hostname
user=$(whoami)
host=$(hostname -s)
# Get current working directory basename for display
if [ -n "$current_dir" ]; then
dir_name=$(basename "$current_dir")
else
dir_name=$(basename "$(pwd)")
fi
# Get git status if we're in a git repo
git_info=""
if git rev-parse --git-dir > /dev/null 2>&1; then
branch=$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null)
if [ -n "$branch" ]; then
if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then
git_info=" ${branch}*"
else
git_info=" ${branch}"
fi
fi
fi
# Get current time in 12-hour format (no seconds)
current_time=$(date '+%I:%M %p')
# Bar with optional target marker (│) showing where even pacing would be
# Usage: make_bar <pct> [target_pct] [color_code] [width=10]
make_bar() {
local pct=$1 target=${2:-} color=${3:-} width=${4:-10}
local filled=$(( (pct * width + 50) / 100 )) # round with half-up trick
[ "$filled" -gt "$width" ] && filled=$width
local target_pos=-1
if [ -n "$target" ] && [ "$target" -ge 0 ] 2>/dev/null && [ "$target" -lt 100 ]; then
target_pos=$(( (target * width + 50) / 100)) # round with half-up trick
[ "$target_pos" -gt "$width" ] && target_pos=$width
fi
local bar=""
for ((i=0; i<width; i++)); do
if [ "$i" -eq "$target_pos" ]; then
#bar="${bar}\033[38;5;199m│\033[0m${color}"
bar="${bar}"
elif [ "$i" -lt "$filled" ]; then
bar="${bar}"
else
bar="${bar}"
fi
done
printf "%s" "$bar"
}
color_for_pct() {
local pct=$1
if [ "$pct" -ge 80 ]; then
printf "\033[91m" # bright red
elif [ "$pct" -ge 50 ]; then
printf "\033[33m" # yellow
else
printf "\033[2m\033[32m" # green, dim
fi
}
CTX_COLOR=$(color_for_pct "$context_pct")
CTX_BAR=$(make_bar "$context_pct" "" "$CTX_COLOR")
# --- Usage limits (5-hour and 7-day) from Anthropic API ---
USAGE_CACHE="/tmp/claude-statusline-usage.json"
USAGE_CACHE_AGE=60 # refresh every 60 seconds
fetch_usage() {
local creds token response
if [[ "$OSTYPE" == "darwin"* ]]; then
creds=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null) || return 1
else
creds=$(<~/.claude/.credentials.json)
fi
token=$(echo "$creds" | jq -r '.claudeAiOauth.accessToken') || return 1
[ -z "$token" ] || [ "$token" = "null" ] && return 1
response=$(curl -s --max-time 3 "https://api.anthropic.com/api/oauth/usage" \
-H "Authorization: Bearer $token" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "Content-Type: application/json" 2>/dev/null) || return 1
# Check for errors
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
return 1
fi
echo "$response" > "$USAGE_CACHE"
}
# Refresh cache if stale or missing
if [ ! -f "$USAGE_CACHE" ] || [ $(($(date +%s) - $(stat -c%Y "$USAGE_CACHE" 2>/dev/null || echo 0))) -gt $USAGE_CACHE_AGE ]; then
fetch_usage 2>/dev/null
fi
# Read cached usage data and calculate pacing targets
usage_5h=""
usage_7d=""
target_5h=""
target_7d=""
if [ -f "$USAGE_CACHE" ]; then
usage_5h=$(jq -r '.five_hour.utilization // empty' "$USAGE_CACHE" 2>/dev/null | cut -d. -f1)
usage_7d=$(jq -r '.seven_day.utilization // empty' "$USAGE_CACHE" 2>/dev/null | cut -d. -f1)
# Calculate pacing targets: how far through each window are we?
NOW_EPOCH=$(date +%s)
# 5-hour window target
resets_5h=$(jq -r '.five_hour.resets_at // empty' "$USAGE_CACHE" 2>/dev/null)
if [ -n "$resets_5h" ]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
reset_epoch=$(date -juf "%Y-%m-%dT%H:%M:%S" "$(echo "$resets_5h" | cut -d. -f1 | sed 's/+.*//')" +%s 2>/dev/null || date -d "$resets_5h" +%s 2>/dev/null)
else
reset_epoch=$(date -ud $resets_5h +%s 2>/dev/null)
fi
if [ -n "$reset_epoch" ]; then
window_secs=$((5 * 3600)) # 5 hours
window_secs_2=$((window_secs / 2))
start_epoch=$((reset_epoch - window_secs))
elapsed=$((NOW_EPOCH - start_epoch))
[ "$elapsed" -lt 0 ] && elapsed=0
[ "$elapsed" -gt "$window_secs" ] && elapsed=$window_secs
target_5h=$(( (elapsed * 100 + window_secs_2) / window_secs)) # round with half-up trick
# epochs seem to always end on the hour, but sometimes the time says 1:59 rather than 2:00; just showing the hour won't work. Round:
if [[ "$OSTYPE" == "darwin"* ]]; then
resets_5h_label=$(date -r "$(( (reset_epoch + 1800) / 3600 * 3600 ))" '+%-l%p' | tr '[:upper:]' '[:lower:]' | tr -d ' ')
else
resets_5h_label=$(date -d "@$(( (reset_epoch + 1800) / 3600 * 3600 ))" +%-Hh)
fi
fi
fi
# 7-day window target
resets_7d=$(jq -r '.seven_day.resets_at // empty' "$USAGE_CACHE" 2>/dev/null)
if [ -n "$resets_7d" ]; then
if [[ "$OSTYPE" == "darwin"* ]]; then
reset_epoch=$(date -juf "%Y-%m-%dT%H:%M:%S" "$(echo "$resets_7d" | cut -d. -f1 | sed 's/+.*//')" +%s 2>/dev/null || date -d "$resets_7d" +%s 2>/dev/null)
else
reset_epoch=$(date -ud $resets_7d +%s 2>/dev/null)
fi
if [ -n "$reset_epoch" ]; then
window_secs=$((7 * 86400))
window_secs_2=$((window_secs / 2))
start_epoch=$((reset_epoch - window_secs))
elapsed=$((NOW_EPOCH - start_epoch))
[ "$elapsed" -lt 0 ] && elapsed=0
[ "$elapsed" -gt "$window_secs" ] && elapsed=$window_secs
target_7d=$(( (elapsed * 100 + window_secs_2) / window_secs)) # round with half-up trick
if [[ "$OSTYPE" == "darwin"* ]]; then
resets_7d_label=$(date -r "$(( (reset_epoch + 1800) / 3600 * 3600 ))" '+%a, %-l%p' | tr '[:upper:]' '[:lower:]' | sed 's/ //2')
else
resets_7d_label=$(date -d "@$(( (reset_epoch + 1800) / 3600 * 3600 ))" '+%a:%-kh')
fi
fi
fi
fi
# Build usage parts
usage_parts=""
if [ -n "$usage_5h" ]; then
U5_COLOR=$(color_for_pct "$usage_5h")
U5_BAR=$(make_bar "$usage_5h" "$target_5h" "$U5_COLOR")
reset_label=""
[ -n "$resets_5h_label" ] && reset_label="${resets_5h_label}"
usage_parts="${U5_COLOR}5hr${reset_label} ${U5_BAR} ${usage_5h}%\033[0m"
fi
if [ -n "$usage_7d" ]; then
U7_COLOR=$(color_for_pct "$usage_7d")
U7_BAR=$(make_bar "$usage_7d" "$target_7d" "$U7_COLOR")
reset_7d_label_str=""
[ -n "$resets_7d_label" ] && reset_7d_label_str="${resets_7d_label}"
[ -n "$usage_parts" ] && usage_parts="${usage_parts}\033[2m │ \033[0m"
usage_parts="${usage_parts}${U7_COLOR}wk${reset_7d_label_str} ${U7_BAR} {usage_7d}%\033[0m"
fi
# Single line output
# removed ${current_time} ${context_pct}%
line="\033[2m\033[96m${dir_name}\033[0m\033[2m${git_info}${session_cost}/${model_name}${CTX_COLOR}ctx ${CTX_BAR} ${context_pct}%\033[0m"
if [ -n "$usage_parts" ]; then
line="${line}\033[2m │ \033[0m${usage_parts}"
fi
echo -e "$line"
@thomaslty
Copy link

nice status line setup! this is exactly what I needed, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment