Claude Code status line: displaying real-time API usage
- Published on
- ·10 min read·Updated on March 9, 2026
The problem: how much quota is actually left?
Claude Code is cool, but honestly? Zero visibility into my consumption. Start a session, chat for 30 minutes, then: DING! "Usage limit reached." Annoying.
I wanted real-time visibility. Not just a number - a real dashboard: current model, context used, 5h and 7d limits, time until reset. All in an elegant status line at the terminal bottom.
After weeks of experimenting, I built something that works. Here's how.
Architecture: dynamic status line
Claude Code has a feature called statusLine that runs a custom shell command. This command:
- Gets JSON on stdin (session, context, model)
- Calls the Anthropic API for real consumption
- Returns ANSI-colored text
Simple but powerful.
Configuration: enable the status line
First, in ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0
}
}
What it does:
type: "command": Runs a shell script (not static text)command: Script path (make executable:chmod +x)padding: 0: No extra space (I like lots of text)
The script: ~/.claude/statusline.sh
Here's the complete script. Bash with smart caching:
#!/bin/bash
# Status line script for Claude Code
# Displays: model + context + real-time API usage
# Receives context data as JSON on stdin
CACHE_FILE="$HOME/.claude/usage_cache.json"
CACHE_TTL=600 # 10 minutes (the API has strict rate limits)
# ANSI colors
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)
# Function: return color based on 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
}
# Function: display progress bar (5 blocks)
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"
}
# Function: format time
format_time() {
local seconds=$1
local hours=$(( seconds / 3600 ))
local minutes=$(( (seconds % 3600) / 60 ))
echo "${hours}h${minutes}m"
}
# Function: fetch API usage
get_api_usage() {
# Check cache (based on timestamp in 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
# Call Anthropic API
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
# Check HTTP status 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 stale cache
if [[ -f "$CACHE_FILE" ]]; then
# Extend the TTL to avoid retrying too quickly
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
# Extract values
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"
}
# Fetch usage
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 // ""')
# Calculate 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=""
# 1. Model (bold indigo)
status+="${BOLD}${INDIGO}◆${RESET} "
status+="${model}"
status+=" │ "
# 2. Context used
ctx_color=$(get_color "$context_pct")
ctx_bar=$(progress_bar "$context_pct")
status+="${ctx_color}Ctx: ${ctx_bar} ${context_pct}%${RESET} │ "
# 3. Time until reset (if available)
if [[ -n "$time_until_reset" ]]; then
status+="${INDIGO}⏱ ${time_until_reset}${RESET} │ "
fi
# 4. 5h usage
five_color=$(get_color "$five_h")
five_bar=$(progress_bar "$five_h")
status+="${five_color}5h: ${five_bar} ${five_h}%${RESET} │ "
# 5. 7d usage
seven_color=$(get_color "$seven_d")
seven_bar=$(progress_bar "$seven_d")
status+="${seven_color}7d: ${seven_bar} ${seven_d}%${RESET} │ "
# 6. Current directory
status+="${INDIGO}⌂ ${work_dir}${RESET}"
echo -e "$status"
How it works: breaking down the script
Reading input data
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)
Claude Code sends JSON with context. We extract:
- Model ("Claude Opus 4.6")
- Context percentage used (0-100)
- Current directory
Calling the Anthropic API
get_api_usage() {
local token=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)
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" ]]; then
# Rate limited or error: reuse stale cache
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 // ""')
}
Four important things:
Auth token: stored in
~/.claude/.credentials.jsonunder theclaudeAiOauth.accessTokenkey (generated by Claude Code at login). OAuth token.Special header:
anthropic-beta: oauth-2025-04-20- a "beta" API for OAuth. Still current with Claude Code 2.1.x.HTTP status code check:
curl -w "\n%{http_code}"captures the status. Without this,curlreturns exit code 0 even on 429 (rate limit), and you end up caching zeros.Real limits:
five_hour.utilization: percentage for last 5h (0-100)seven_day.utilization: percentage for last 7d (0-100)five_hour.resets_at: ISO timestamp when 5h window resets
Smart caching
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
API called once every 10 minutes max (CACHE_TTL=600). This matters: the /api/oauth/usage endpoint has strict rate limits. With a TTL that's too short (like 60s), you'll get rate-limited in a loop and end up showing 0% forever.
Cache stored in ~/.claude/usage_cache.json with a timestamp in the JSON. Key point: on API errors (429, timeout...), we reuse stale cache instead of overwriting it with zeros. Without this, a single rate limit is enough to show 0% until the next successful call.
Progress bars
progress_bar() {
local pct=$1
local filled=$(( pct / 20 ))
local bar=""
for (( i=0; i<filled; i++ )); do
bar+="▰"
done
for (( i=filled; i<5; i++ )); do
bar+="▱"
done
echo "$bar"
}
Each bar = 5 blocks, so each block = 20%.
▰▰▱▱▱ = 40%
▰▰▰▱▱ = 60%
▰▰▰▰▰ = 100%
Color coding by threshold
get_color() {
local pct=$1
if (( $(echo "$pct < 50" | bc -l) )); then
echo -e "$CYAN" # Green: safe
elif (( $(echo "$pct < 80" | bc -l) )); then
echo -e "$VIOLET" # Violet: warning
else
echo -e "$MAGENTA" # Pink: danger
fi
}
- < 50%: Cyan (relaxed)
- 50-80%: Violet (getting tight)
80%: Magenta (red alert!)
Example output
Here's what you see at the bottom of your terminal:
◆ Claude Opus 4.6 │ Ctx: ▰▰▱▱▱ 35% │ ⏱ 3h42m │ 5h: ▰▰▰▱▱ 52% │ 7d: ▰▱▱▱▱ 18% │ ⌂ fransys-blog
Decoding:
- ◆ Claude Opus 4.6: I'm on full Opus
- Ctx: ▰▰▱▱▱ 35%: Used 35% of context window (good, not saturated)
- ⏱ 3h42m: Reset in 3h42m
- 5h: ▰▰▰▱▱ 52%: 52% of 5h limit used
- 7d: ▰▱▱▱▱ 18%: 18% of 7d limit used (very comfortable)
- ⌂ fransys-blog: In the fransys-blog directory
Lots of headroom. Good, continue.
Customization: adapt the colors
ANSI color codes:
\033[38;5;54m= Indigo (my primary)\033[38;5;51m= Cyan\033[38;5;141m= Violet\033[38;5;201m= Magenta
256 ANSI colors vary by terminal. Test first:
for i in {0..255}; do echo -e "\033[38;5;${i}m█\033[0m"; done
You'll see each code visually. Modify INDIGO, CYAN, etc. based on your taste.
Common troubleshooting
Status line doesn't appear:
- Check script is executable:
chmod +x ~/.claude/statusline.sh - Test manually:
echo '{"model": {"display_name": "test"}, "context_window": {"used_percentage": 50}}' | ~/.claude/statusline.sh
API values are 0:
- Check token:
jq '.claudeAiOauth.accessToken' ~/.claude/.credentials.json - If empty, re-auth:
claude auth login - Test the API manually to check for rate limiting:
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"
If you see HTTP:429, that's a rate limit. Wait a few minutes and increase CACHE_TTL. See "The rate limit trap" below.
The rate limit trap (permanent 0%):
The most common issue. The /api/oauth/usage endpoint has strict rate limits. If your script calls it too often:
- API returns HTTP 429 (rate limited)
curlstill returns exit code 0 (the connection itself succeeded)jqparses the error response,.five_hour.utilizationdoesn't exist -> falls back to0- The
0gets cached -> displayed for the entire TTL - Cache expires -> retry -> rate-limited again -> infinite loop of 0%
The fix: check the HTTP status code (curl -w "%{http_code}"), never cache error responses, and keep a long enough TTL (600s minimum)
Color isn't right:
- Terminal might lack 256 colors. Test:
echo $TERM. Should sayxterm-256coloror better. - Old terminal? Replace
38;5;XXcodes with classics:31(red),32(green), etc.
Advanced use cases
Logging consumption:
echo "$(date) - 5h: $(echo "$usage" | jq '.five_hour')% | 7d: $(echo "$usage" | jq '.seven_day')%" >> ~/.claude/usage.log
Add to your Stop hook for a history.
Alerts:
if [[ $five_h -gt 85 ]]; then
# Send a notification
notify-send "Claude Code" "5h usage at ${five_h}% - slow down!"
fi
Useful if you're tight on limits.
Show more info:
# Also show sub-agent model
status+=" [subagent: ${CYAN}haiku${RESET}]"
That's it
This status line transformed my relationship with Claude Code. No more fear: I see exactly where I stand, how much time before reset, and can make smart decisions ("10,000 thinking tokens on this problem or keep it simple?").
The script isn't perfect - could be more robust, faster than bash - but it's a solid start. Copy it, adapt colors to your terminal theme, adjust alert thresholds to your limits.
Previous post
← Managing photos with self-hosted Immich