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 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 (but in color):

yearone-3 main* │ $3.45/Opus 4.6 │ ctx ▓▓░░░░░░ 24% │ 5hr➞17:23 ▓▓▓│░░░░ 45% │ wk➞Thu:5p ▓▓░│░░░░ 14%
  • current directory
  • git branch with clean/dirty indicator
  • Color-coded bars: green (<45%), yellow (45-70%), bright red (>70%)
  • White pacing marker showing where you should be for even usage across the window
  • Reset times so you know when your limits refill

Prerequisites

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

Setup

Step 1: Create the status line script

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

Step 2: Configure Claude Code

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

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

For most people, you're done! MacOS, read on. To verify, see step 5.

Step 4: Ensure your keychain has a valid token

Linux

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

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.

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 Cyan \033[32m
Pacing marker Bright white \033[1;37m
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

Use the width argument to make_bar() to make bars wider or narrower than 8 characters (default is 10 if not provided)

Cache interval

Change USAGE_CACHE_AGE=180 to control how often the API is called (in seconds); less will likely get you rate-limited.

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 120-second TTL

MacOS credentials: 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
  • Original context window calculation inspired by Richard-Weiss
  • Original version by patyearone
#!/bin/bash
# claude code status bar that shows:
# - current directory
# - git branch & clean/dirty status
# - current session cost (in $, inferred if on subscription)
# - current model
# - context usage as a bar and pct
# - usage and pacing target for 5 hour subscription usage window
# - usage and pacing target for 7 day subscription usage window
# - current time (disabled)
# you can rearrange these items at the bottom if you desire
# Read Claude Code context from stdin
input=$(cat)
# linux and macos supported
[[ "$OSTYPE" == "linux-gnu"* ]] || [[ "$OSTYPE" == "darwin"* ]] || (echo "Unsupported OS!" && exit)
# 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)"
current_time=$(date '+%I:%M %p') # time in 12-hour format (no seconds)
# 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
color_for_pct() {
local pct=$1
if [ "$pct" -ge 70 ]; then
printf "\033[0;91m" # bright red
elif [ "$pct" -ge 45 ]; then
printf "\033[0;33m" # yellow
else
printf "\033[2;32m" # dim, green
fi
}
# Bar with optional target marker (│) showing where even pacing would be.
# Uses fractional blocks for more precision on pct. Draws target_pct in the right block-interval.
# filled portion is colored, non-filled is dimmed & not colored
# Usage: make_bar <pct> [color_code] [target_pct] [width=10]
make_bar() {
local pct=$1
local color=$2
local target=${3:-}
local width=${4:-10}
# Unicode fractional blocks
local blocks=( "" ▏ ▎ ▍ ▌ ▋ ▊ ▉ )
# progress in 1/8 cells
local subcells=$((width * 8))
local filled=$((pct * subcells / 100))
local full=$((filled / 8))
local rem=$((filled % 8))
# optional pacing marker
local target_pos=-1
if [[ -n "$target" && "$target" =~ ^[0-9]+$ ]]; then
target_pos=$((target * width / 100)) # takes floor of value
((target_pos >= width)) && target_pos=$((width-1)) # (applies if target>=100) => value in 0..width-1
fi
local bar=""
for ((i=0;i<width;i++)); do
# draw marker at target boundary
if ((i == target_pos)); then
#bar+="\033[0m│" # grey target bar
bar+="\033[22;1;37m│" # bright white target bar
elif ((i < full)); then
bar+="${color}"
elif ((i == full)); then
bar+="${color}${blocks[$rem]}"
else
bar+="\033[0;2m░"
fi
done
printf "%s" "$bar"
}
CTX_COLOR=$(color_for_pct "$context_pct")
CTX_BAR=$(make_bar "$context_pct" "$CTX_COLOR" "" 8)
# --- Usage limits (5-hour and 7-day) from Anthropic API ---
USAGE_CACHE="/tmp/claude-statusline-usage.json"
USAGE_CACHE_AGE=180 # refresh every 180 seconds max; don't do more often
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 2
[ -z "$token" ] || [ "$token" = "null" ] && return 3
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 4
# Check for errors
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
return 5
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
# 5hr epochs used to end on the hour, but not anymore
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) # rounded to 24H clock hour, followed by 'h'
resets_5h_label=$(date -d "@$reset_epoch" +%H:%M) # 24H clock HH:MM
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
# 7d epochs seems to end on the hour but the time may say 1:59 rather than 2:00; just showing the hour won't work. Round:
if [[ "$OSTYPE" == "darwin"* ]]; then
resets_7d_label=$(date -r "$(( (reset_epoch + 1800) / 3600 * 3600 ))" '+%a:%-l%p') | tr '[:upper:]' '[:lower:]' # rounded to 12H clock hour (lowercase) w/, eg tue:4pm
else
# resets_7d_label=$(date -d "@$reset_epoch" '+%a %H:%M') # eg 'Tue 15:55'
# resets_7d_label=$(date -d "@$(( (reset_epoch + 1800) / 3600 * 3600 ))" '+%a:%-kh') # rounded to 24H clock hour, eg Tue:16h
resets_7d_label=$(date -d "@$(( (reset_epoch + 1800) / 3600 * 3600 ))" '+%a:%-l%P') # rounded to 12H clock hour w/, eg Tue:4pm
resets_7d_label=${resets_7d_label:0:-1} # strip the last character, eg Tue:4p
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" "$U5_COLOR" "$target_5h" 8)
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" "$U7_COLOR" "$target_7d" 8)
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}\033[0m" # removed ${usage_7d}%
fi
# Single line output
line=""
# current path and git status
line+="\033[0;22m\033[32m${dir_name}\033[0;2m${git_info}"
# session cost and current model name
line+="${session_cost}/${model_name}"
# model context status bar
line+="${CTX_COLOR}ctx ${CTX_BAR} ${context_pct}%\033[0m"
# 5 hour and 7-day usage bars
if [ -n "$usage_parts" ]; then
line+="\033[2m │ \033[0m${usage_parts}"
fi
# current time
#line+=" | ${current_time}"
echo -e "$line"
@thomaslty

Copy link
Copy Markdown

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