Claude Code status line: displaying real-time API usage

Claude Code status line: displaying real-time API usage

·8 min read·Updated on March 9, 2026

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.6Ctx: ▰▰▱▱▱ 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:

  1. The API returns HTTP 429
  2. curl still returns exit code 0 (the connection succeeded)
  3. jq parses the error response, .five_hour.utilization doesn't exist → falls back to 0
  4. The 0 gets cached → displayed for the whole TTL
  5. 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.

ShareLinkedInXBluesky

Related articles