
Claude Code status line: displaying real-time API usage
The problem: how much quota is actually left?
Claude Code offers no visibility into your consumption. You start a session, work for thirty minutes, and "Usage limit reached" lands without warning.
The goal: a real-time view — current model, context used, 5h and 7d limits, time until reset — in a status line at the bottom of the terminal.
Architecture
Claude Code has a statusLine feature that runs a custom shell command. That command receives JSON on stdin (session, context, model), can call the Anthropic API for real usage, and returns coloured ANSI text.
In ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0
}
}
type: "command" runs a shell script (not static text), command points at the script (make it executable with chmod +x), and padding: 0 removes extra spacing.
The script
#!/bin/bash
# Status line script for Claude Code
# Displays: model + context + real-time API usage
# Receives data as JSON on stdin
CACHE_FILE="$HOME/.claude/usage_cache.json"
CACHE_TTL=600 # 10 minutes (the API has a strict rate limit)
# ANSI colours
INDIGO="\033[38;5;54m"
CYAN="\033[38;5;51m"
VIOLET="\033[38;5;141m"
MAGENTA="\033[38;5;201m"
RESET="\033[0m"
BOLD="\033[1m"
# Read JSON data from stdin
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name // "Unknown"')
context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0')
work_dir=$(echo "$input" | jq -r '.workspace.current_dir // "~"' | xargs basename)
# Return the colour for a percentage
get_color() {
local pct=$1
if (( $(echo "$pct < 50" | bc -l) )); then
echo -e "$CYAN"
elif (( $(echo "$pct < 80" | bc -l) )); then
echo -e "$VIOLET"
else
echo -e "$MAGENTA"
fi
}
# Render a progress bar (5 blocks, 20% each)
progress_bar() {
local pct=$1
local blocks=5
local filled=$(( pct / 20 ))
[[ $filled -gt $blocks ]] && filled=$blocks
local bar=""
for (( i=0; i<filled; i++ )); do
bar+="▰"
done
for (( i=filled; i<blocks; i++ )); do
bar+="▱"
done
echo "$bar"
}
format_time() {
local seconds=$1
local hours=$(( seconds / 3600 ))
local minutes=$(( (seconds % 3600) / 60 ))
echo "${hours}h${minutes}m"
}
# Fetch API usage
get_api_usage() {
# Check the cache (based on the timestamp inside the JSON)
if [[ -f "$CACHE_FILE" ]]; then
local cache_time=$(jq -r '.timestamp // 0' "$CACHE_FILE" 2>/dev/null)
local cache_age=$(( $(date +%s) - cache_time ))
if [[ $cache_age -lt $CACHE_TTL ]]; then
cat "$CACHE_FILE"
return 0
fi
fi
# OAuth token generated by Claude Code at login
local token=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)
if [[ -z "$token" ]]; then
echo '{"five_hour": 0, "seven_day": 0, "reset_time": ""}'
return 1
fi
# Capture the HTTP code to detect errors (429, 401, etc.)
local http_response=$(curl -s -w "\n%{http_code}" --max-time 5 \
-H "Authorization: Bearer $token" \
-H "anthropic-beta: oauth-2025-04-20" \
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
local http_code=$(echo "$http_response" | tail -1)
local response=$(echo "$http_response" | sed '$d')
if [[ "$http_code" != "200" ]] || [[ -z "$response" ]]; then
# API error (rate limit, etc.): reuse the old cache
if [[ -f "$CACHE_FILE" ]]; then
# Extend the TTL to avoid retrying too soon
local old_data=$(cat "$CACHE_FILE")
echo "$old_data" | jq --arg ts "$(date +%s)" '.timestamp = ($ts | tonumber)' > "$CACHE_FILE"
cat "$CACHE_FILE"
else
echo '{"five_hour": 0, "seven_day": 0, "reset_time": ""}'
fi
return 1
fi
local five_h=$(echo "$response" | jq -r '.five_hour.utilization // 0')
local seven_d=$(echo "$response" | jq -r '.seven_day.utilization // 0')
local reset_ts=$(echo "$response" | jq -r '.five_hour.resets_at // ""')
local result='{"five_hour": '"$five_h"', "seven_day": '"$seven_d"', "reset_time": "'"$reset_ts"'", "timestamp": '"$(date +%s)"'}'
echo "$result" > "$CACHE_FILE"
echo "$result"
}
usage=$(get_api_usage)
five_h=$(echo "$usage" | jq -r '.five_hour // 0')
seven_d=$(echo "$usage" | jq -r '.seven_day // 0')
reset_time=$(echo "$usage" | jq -r '.reset_time // ""')
# Time until reset
time_until_reset=""
if [[ -n "$reset_time" && "$reset_time" != "null" ]]; then
reset_epoch=$(date -d "$reset_time" +%s 2>/dev/null || echo 0)
now_epoch=$(date +%s)
diff=$(( reset_epoch - now_epoch ))
if [[ $diff -gt 0 ]]; then
time_until_reset=$(format_time $diff)
fi
fi
# Build the status line
status=""
status+="${BOLD}${INDIGO}◆${RESET} ${model} │ "
ctx_color=$(get_color "$context_pct")
ctx_bar=$(progress_bar "$context_pct")
status+="${ctx_color}Ctx: ${ctx_bar} ${context_pct}%${RESET} │ "
if [[ -n "$time_until_reset" ]]; then
status+="${INDIGO}⏱ ${time_until_reset}${RESET} │ "
fi
five_color=$(get_color "$five_h")
five_bar=$(progress_bar "$five_h")
status+="${five_color}5h: ${five_bar} ${five_h}%${RESET} │ "
seven_color=$(get_color "$seven_d")
seven_bar=$(progress_bar "$seven_d")
status+="${seven_color}7d: ${seven_bar} ${seven_d}%${RESET} │ "
status+="${INDIGO}⌂ ${work_dir}${RESET}"
echo -e "$status"
The four points that matter
The auth token lives in ~/.claude/.credentials.json under claudeAiOauth.accessToken, generated by Claude Code at login. The call requires the anthropic-beta: oauth-2025-04-20 header (still current with Claude Code 2.1.x).
The real limits come from three fields: five_hour.utilization (5h percentage), seven_day.utilization (7d percentage), and five_hour.resets_at (ISO reset timestamp).
Caching limits the API to one call every 10 minutes (CACHE_TTL=600). The /api/oauth/usage endpoint has a strict rate limit: with too short a TTL you get rate-limited in a loop. Crucially, on API error the old cache is reused rather than overwritten with zeros. Without that, a single rate limit shows 0% until the next successful call.
Threshold colour coding: cyan below 50% (comfortable), violet between 50 and 80% (tightening), magenta above (alert). Each bar is 5 blocks, so 20% per block:
▰▰▱▱▱ = 40%
▰▰▰▱▱ = 60%
▰▰▰▰▰ = 100%
Example output
◆ Claude Opus 4.6 │ Ctx: ▰▰▱▱▱ 35% │ ⏱ 3h42m │ 5h: ▰▰▰▱▱ 52% │ 7d: ▰▱▱▱▱ 18% │ ⌂ fransys-blog
Full Opus model, 35% of the context window used, reset in 3h42m, 52% of the 5h limit and 18% of the 7d limit consumed, in the fransys-blog directory. Plenty of headroom.
Troubleshooting
The status line doesn't appear: check the script is executable (chmod +x ~/.claude/statusline.sh) and test it by hand:
echo '{"model": {"display_name": "test"}, "context_window": {"used_percentage": 50}}' | ~/.claude/statusline.sh
API values are 0: check the token with jq '.claudeAiOauth.accessToken' ~/.claude/.credentials.json, re-authenticate with claude auth login if empty, then test the API:
TOKEN=$(jq -r '.claudeAiOauth.accessToken' ~/.claude/.credentials.json)
curl -s -w "\nHTTP:%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
-H "anthropic-beta: oauth-2025-04-20" \
"https://api.anthropic.com/api/oauth/usage"
The rate-limit trap (permanent 0%) is the most common problem. The mechanism:
- The API returns HTTP 429
curlstill returns exit code 0 (the connection succeeded)jqparses the error response,.five_hour.utilizationdoesn't exist → falls back to0- The
0gets cached → displayed for the whole TTL - Cache expires → retry → rate-limited again → infinite 0% loop
The fix: capture the HTTP code (curl -w "%{http_code}"), never cache errors, and keep the TTL at 600s or more.
Wrong colours: the terminal may not support 256 colours (echo $TERM should say xterm-256color or better). On an old terminal, replace the 38;5;XX codes with classics (31 red, 32 green). To view the palette:
for i in {0..255}; do echo -e "\033[38;5;${i}m█\033[0m"; done
Extensions
Log consumption from a Stop hook to keep a history:
echo "$(date) - 5h: $(echo "$usage" | jq '.five_hour')% | 7d: $(echo "$usage" | jq '.seven_day')%" >> ~/.claude/usage.log
Or fire a system alert on a threshold:
if [[ $five_h -gt 85 ]]; then
notify-send "Claude Code" "5h usage at ${five_h}% - slow down!"
fi
The script isn't perfect — it would benefit from a faster language than bash — but it delivers exactly what was missing: knowing where you stand, how long until reset, and being able to decide knowingly between 10,000 thinking tokens and a simple pass.
Related articles