Skip to content

Instantly share code, notes, and snippets.

@patyearone
Last active April 20, 2026 20:32
Show Gist options
  • Select an option

  • Save patyearone/7c753ef536a49839c400efaf640e17de to your computer and use it in GitHub Desktop.

Select an option

Save patyearone/7c753ef536a49839c400efaf640e17de 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 pacing, reset times, and context window usage — all from data Claude Code already provides via stdin.

What you get:

your-project │ ctx 0% │ 5hr (3pm) 16/65% │ wk (fri,12pm) 44/57% │ Opus 4.6 (1M context)
  • your-project — current working directory name
  • ctx 0% — context window usage
  • 5hr (3pm) 16/65% — 5-hour window: 16% used, 65% of window elapsed (pace target), resets at 3pm
  • wk (fri,12pm) 44/57% — 7-day window: 44% used, 57% of window elapsed, resets Friday at 12pm
  • Color-coded by pacing: green (under pace), yellow (at/slightly over), red (>10% over pace)
  • Pace target shown in hot pink after the /

Prerequisites

  • macOS (uses BSD date — Linux adaptation notes at the bottom)
  • jq installed (brew install jq)
  • Claude Code v2.1+ (must provide rate_limits in status line stdin JSON)

How It Works

Claude Code pipes a JSON object to your status line script via stdin on every render. As of v2.1+, this JSON includes a rate_limits field with your current usage percentages and reset timestamps — no API calls, OAuth tokens, or caching needed.

The stdin JSON

Claude Code provides these fields (among others):

{
  "model": { "display_name": "Opus 4.6 (1M context)" },
  "workspace": { "current_dir": "/Users/you/project" },
  "context_window": { "used_percentage": 12.5 },
  "rate_limits": {
    "five_hour": {
      "used_percentage": 16.0,
      "resets_at": 1743973200
    },
    "seven_day": {
      "used_percentage": 44.0,
      "resets_at": 1744470000
    }
  }
}
  • used_percentage — how much of your limit you've consumed (0–100)
  • resets_at — Unix epoch when the window resets

Pacing target calculation

The "pace" number tells you what percentage of the window's time has elapsed — i.e., where you'd be if you spread usage evenly across the entire window. If your usage is below the pace, you're in good shape.

window_start = resets_at - window_duration
elapsed = now - window_start
pace = elapsed / window_duration × 100

For the 5-hour window: window_duration = 18,000 seconds For the 7-day window: window_duration = 604,800 seconds

Color logic

Colors compare your actual usage against the pace target, not absolute thresholds:

Condition Color Meaning
usage < pace Green Under pace — you're fine
usage ≥ pace AND usage ≤ pace × 1.1 Yellow At pace or slightly over (within 10%)
usage > pace × 1.1 Red More than 10% over pace — slow down

This is more useful than fixed thresholds because 60% usage is fine if 80% of the window has passed, but alarming if only 20% has.

Setup

Step 1: Create the status line script

Save this as ~/.claude/statusline-command.sh:

#!/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 // ""')
context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)

# Basename of current directory (p10k Pure \W style: blue folder name)
if [ -n "$current_dir" ]; then
    dir_display=$(basename "$current_dir")
else
    dir_display=$(basename "$HOME")
fi

# Get git status if we're in a git repo
# Green = clean, bright yellow = uncommitted changes (staged, unstaged, or untracked)
git_info=""
git_color=""
repo_dir="${current_dir:-$HOME}"
if git -C "$repo_dir" --no-optional-locks rev-parse --git-dir > /dev/null 2>&1; then
    branch=$(git -C "$repo_dir" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null || git -C "$repo_dir" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
    if [ -n "$branch" ]; then
        if ! git -C "$repo_dir" --no-optional-locks diff --quiet 2>/dev/null || \
           ! git -C "$repo_dir" --no-optional-locks diff --cached --quiet 2>/dev/null || \
           [ -n "$(git -C "$repo_dir" --no-optional-locks ls-files --others --exclude-standard 2>/dev/null)" ]; then
            git_color="\033[93m"   # bright yellow — dirty
            git_info=" ${branch}*"
        else
            git_color="\033[32m"   # green — clean
            git_info=" ${branch}"
        fi
    fi
fi

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[32m"  # green
    fi
}

CTX_COLOR=$(color_for_pct "$context_pct")

# --- Usage limits from Claude Code's rate_limits field ---
usage_5h=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty' 2>/dev/null | cut -d. -f1)
usage_7d=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty' 2>/dev/null | cut -d. -f1)

# Calculate pacing targets using resets_at from the rate_limits data
NOW_EPOCH=$(date +%s)
target_5h=""
target_7d=""
resets_5h_label=""
resets_7d_label=""

if [ -n "$usage_5h" ]; then
    resets_5h=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty' 2>/dev/null)
    if [ -n "$resets_5h" ]; then
        reset_epoch=$resets_5h
        if [ -n "$reset_epoch" ]; then
            window_secs=$((5 * 3600))
            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))
            resets_5h_label=$(date -d "@${reset_epoch}" '+%I%p' 2>/dev/null | sed 's/^0//' | tr '[:upper:]' '[:lower:]')
            # Fallback for macOS
            [ -z "$resets_5h_label" ] && resets_5h_label=$(date -r "$reset_epoch" '+%-I%p' 2>/dev/null | tr '[:upper:]' '[:lower:]')
        fi
    fi
fi

if [ -n "$usage_7d" ]; then
    resets_7d=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty' 2>/dev/null)
    if [ -n "$resets_7d" ]; then
        reset_epoch=$resets_7d
        if [ -n "$reset_epoch" ]; then
            window_secs=$((7 * 86400))
            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))
            resets_7d_label=$(date -d "@${reset_epoch}" '+%a,%-I%p' 2>/dev/null | tr '[:upper:]' '[:lower:]')
            # Fallback for macOS
            [ -z "$resets_7d_label" ] && resets_7d_label=$(date -r "$reset_epoch" '+%a,%-I%p' 2>/dev/null | tr '[:upper:]' '[:lower:]')
        fi
    fi
fi

# Color actual usage vs pace (percentage-based threshold, integer math):
#   green  — usage < pace
#   yellow — usage >= pace AND usage * 100 <= pace * 110  (within 10% over pace)
#   red    — usage * 100 > pace * 110  (more than 10% over pace)
color_for_usage_vs_pace() {
    local usage=$1 pace=$2
    if [ -n "$pace" ] && [ $((usage * 100)) -gt $((pace * 110)) ]; then
        printf "\033[91m"   # red — more than 10% over pace
    elif [ -n "$pace" ] && [ "$usage" -ge "$pace" ]; then
        printf "\033[33m"   # yellow — over pace but within 10%
    else
        printf "\033[32m"   # green — under pace
    fi
}

# Build usage parts — compact numeric format: "5hr (12pm) 30/50%"
usage_parts=""
if [ -n "$usage_5h" ]; then
    U5_COLOR=$(color_for_usage_vs_pace "$usage_5h" "$target_5h")
    reset_label=""
    [ -n "$resets_5h_label" ] && reset_label="(${resets_5h_label}) "
    pace_part=""
    [ -n "$target_5h" ] && pace_part="\033[38;5;199m/${target_5h}"
    usage_parts="${U5_COLOR}5hr ${reset_label}${usage_5h}${pace_part}%\033[0m"
fi
if [ -n "$usage_7d" ]; then
    U7_COLOR=$(color_for_usage_vs_pace "$usage_7d" "$target_7d")
    reset_7d_label_str=""
    [ -n "$resets_7d_label" ] && reset_7d_label_str="(${resets_7d_label}) "
    pace_part_7d=""
    [ -n "$target_7d" ] && pace_part_7d="\033[38;5;199m/${target_7d}"
    [ -n "$usage_parts" ] && usage_parts="${usage_parts}\033[2m │ \033[0m"
    usage_parts="${usage_parts}${U7_COLOR}wk ${reset_7d_label_str}${usage_7d}${pace_part_7d}%\033[0m"
fi

# Single line output
# blue(57C7FF) dir · green/yellow git branch · ctx% · usage parts · model
line="\033[38;2;87;199;255m${dir_display}\033[0m${git_color}${git_info}\033[0m\033[2m │ ${CTX_COLOR}ctx ${context_pct}%\033[0m"
if [ -n "$usage_parts" ]; then
    line="${line}\033[2m │ \033[0m${usage_parts}"
fi
line="${line}\033[2m │ ${model_name}\033[0m"
printf "%b\n" "$line"

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: Verify

# Test with mock data that simulates what Claude Code sends
echo '{
  "model": {"display_name": "Opus 4.6 (1M context)"},
  "workspace": {"current_dir": "/tmp/test-project"},
  "context_window": {"used_percentage": 20},
  "rate_limits": {
    "five_hour": {"used_percentage": 37, "resets_at": '"$(($(date +%s) + 7200))"'},
    "seven_day": {"used_percentage": 26, "resets_at": '"$(($(date +%s) + 259200))"'}
  }
}' | bash ~/.claude/statusline-command.sh

Understanding the Output

your-project │ ctx 0% │ 5hr (3pm) 16/65% │ wk (fri,12pm) 44/57% │ Opus 4.6 (1M context)

Reading 5hr (3pm) 16/65%:

  • 5hr — the 5-hour rolling window
  • (3pm) — window resets at 3pm local time
  • 16 — you've used 16% of your 5-hour limit
  • /65 — 65% of the 5-hour window has elapsed (the pace target, shown in hot pink)
  • % — both numbers are percentages

If usage (16) < pace (65), you're well under pace → green. You could use 4× your current rate and still not hit the limit before the window resets.

Customization

Colors

Element Current ANSI Code
Directory Blue (#57C7FF) \033[38;2;87;199;255m
Git clean Green \033[32m
Git dirty Bright yellow \033[93m
Pace target number Hot pink \033[38;5;199m
Under pace Green \033[32m
At pace (within 10%) Yellow \033[33m
Over pace (>10%) Bright red \033[91m
Context <50% Green \033[32m
Context 50-80% Yellow \033[33m
Context >80% Bright red \033[91m

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

Available stdin 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
rate_limits.five_hour.used_percentage 5-hour window usage (0–100)
rate_limits.five_hour.resets_at 5-hour reset time (Unix epoch)
rate_limits.seven_day.used_percentage 7-day window usage (0–100)
rate_limits.seven_day.resets_at 7-day reset time (Unix epoch)
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)

Linux adaptation

The script tries Linux-style date -d first with macOS date -r as fallback. If you're Linux-only, you can drop the fallback lines. The only OS-specific part is date formatting:

  • macOS: date -r $epoch '+%-I%p' (convert epoch to human time)
  • Linux: date -d "@$epoch" '+%I%p' | sed 's/^0//' (same thing)

No keychain, no curl, no platform-specific credential storage needed.

Troubleshooting

Quick diagnostic

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

# 2. Does the script run without errors?
echo '{"model":{"display_name":"Test"},"workspace":{"current_dir":"/tmp"},"context_window":{"used_percentage":42},"rate_limits":{"five_hour":{"used_percentage":30,"resets_at":'"$(($(date +%s)+3600))"'},"seven_day":{"used_percentage":50,"resets_at":'"$(($(date +%s)+86400))"'}}}' \
    | bash ~/.claude/statusline-command.sh 2>&1
echo "exit: $?"

# 3. Does Claude Code provide rate_limits?
# Add this temporarily to the top of your script to capture the JSON:
#   cat > /tmp/cc-statusline-debug.json
# Then check: jq '.rate_limits' /tmp/cc-statusline-debug.json

Common issues

Problem Cause Fix
No status line at all Script exits non-zero Run the diagnostic above — check stderr
Usage shows but no pace numbers resets_at missing from rate_limits Update Claude Code — older versions may not include it
Only ctx shows, no 5hr/wk rate_limits not in stdin JSON Update Claude Code to v2.1+
Reset time label is blank date format incompatible Check if you're on macOS or Linux — the script tries both
Colors look wrong Terminal doesn't support 256-color Most modern terminals do; check echo $TERM
Git branch slow in large repos git diff and git ls-files scanning The --no-optional-locks flag helps; consider caching git info
jq: command not found jq not installed brew install jq

Evolution Notes

This approach replaced an earlier version that called Anthropic's undocumented /api/oauth/usage endpoint with an OAuth token from the macOS Keychain. That worked but required managing credentials, handling token refresh, caching API responses, and dealing with keychain staleness. Claude Code v2.1+ exposes rate_limits directly in the status line stdin JSON, eliminating all of that complexity.

Credits

#!/bin/bash
# Claude Code Status Line Script
# Reads rate_limits, context window, and model info from Claude Code's stdin JSON.
# No API calls, no OAuth tokens, no caching needed.
#
# Setup:
# 1. Save this file as ~/.claude/statusline-command.sh
# 2. chmod +x ~/.claude/statusline-command.sh
# 3. Add to ~/.claude/settings.json:
# { "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" } }
# 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 // ""')
context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
# Basename of current directory
if [ -n "$current_dir" ]; then
dir_display=$(basename "$current_dir")
else
dir_display=$(basename "$HOME")
fi
# Git status: green ✓ = clean, bright yellow * = dirty
git_info=""
git_color=""
repo_dir="${current_dir:-$HOME}"
if git -C "$repo_dir" --no-optional-locks rev-parse --git-dir > /dev/null 2>&1; then
branch=$(git -C "$repo_dir" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null || git -C "$repo_dir" --no-optional-locks rev-parse --short HEAD 2>/dev/null)
if [ -n "$branch" ]; then
if ! git -C "$repo_dir" --no-optional-locks diff --quiet 2>/dev/null || \
! git -C "$repo_dir" --no-optional-locks diff --cached --quiet 2>/dev/null || \
[ -n "$(git -C "$repo_dir" --no-optional-locks ls-files --others --exclude-standard 2>/dev/null)" ]; then
git_color="\033[93m" # bright yellow — dirty
git_info=" ${branch}*"
else
git_color="\033[32m" # green — clean
git_info=" ${branch}"
fi
fi
fi
# Context window color: green <50%, yellow 50-80%, red >80%
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[32m" # green
fi
}
CTX_COLOR=$(color_for_pct "$context_pct")
# --- Usage limits from Claude Code's rate_limits field ---
usage_5h=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty' 2>/dev/null | cut -d. -f1)
usage_7d=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty' 2>/dev/null | cut -d. -f1)
# Calculate pacing targets: what % of each window's time has elapsed?
# If usage < pace, you're fine. If usage > pace, you're burning too fast.
NOW_EPOCH=$(date +%s)
target_5h=""
target_7d=""
resets_5h_label=""
resets_7d_label=""
# Helper: convert epoch to human-readable time (works on both macOS and Linux)
epoch_to_time() {
local epoch=$1 fmt=$2
# Try Linux first, then macOS
date -d "@${epoch}" "+${fmt}" 2>/dev/null || date -r "$epoch" "+${fmt}" 2>/dev/null
}
if [ -n "$usage_5h" ]; then
resets_5h=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty' 2>/dev/null)
if [ -n "$resets_5h" ]; then
reset_epoch=$resets_5h
if [ -n "$reset_epoch" ]; then
window_secs=$((5 * 3600))
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))
resets_5h_label=$(epoch_to_time "$reset_epoch" '%-I%p' | tr '[:upper:]' '[:lower:]')
# %-I isn't portable everywhere; fallback strips leading zero manually
[ -z "$resets_5h_label" ] && resets_5h_label=$(epoch_to_time "$reset_epoch" '%I%p' | sed 's/^0//' | tr '[:upper:]' '[:lower:]')
fi
fi
fi
if [ -n "$usage_7d" ]; then
resets_7d=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty' 2>/dev/null)
if [ -n "$resets_7d" ]; then
reset_epoch=$resets_7d
if [ -n "$reset_epoch" ]; then
window_secs=$((7 * 86400))
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))
resets_7d_label=$(epoch_to_time "$reset_epoch" '%a,%-I%p' | tr '[:upper:]' '[:lower:]')
[ -z "$resets_7d_label" ] && resets_7d_label=$(epoch_to_time "$reset_epoch" '%a,%I%p' | sed 's/,0/,/' | tr '[:upper:]' '[:lower:]')
fi
fi
fi
# Color by usage vs pace:
# green — usage < pace (under pace, you're fine)
# yellow — usage >= pace, within 10% over (at pace, watch it)
# red — usage > pace × 1.1 (more than 10% over, slow down)
color_for_usage_vs_pace() {
local usage=$1 pace=$2
if [ -n "$pace" ] && [ $((usage * 100)) -gt $((pace * 110)) ]; then
printf "\033[91m" # red
elif [ -n "$pace" ] && [ "$usage" -ge "$pace" ]; then
printf "\033[33m" # yellow
else
printf "\033[32m" # green
fi
}
# Build usage segments — compact format: "5hr (3pm) 16/65%"
# The pace number after / is shown in hot pink (\033[38;5;199m)
usage_parts=""
if [ -n "$usage_5h" ]; then
U5_COLOR=$(color_for_usage_vs_pace "$usage_5h" "$target_5h")
reset_label=""
[ -n "$resets_5h_label" ] && reset_label="(${resets_5h_label}) "
pace_part=""
[ -n "$target_5h" ] && pace_part="\033[38;5;199m/${target_5h}"
usage_parts="${U5_COLOR}5hr ${reset_label}${usage_5h}${pace_part}%\033[0m"
fi
if [ -n "$usage_7d" ]; then
U7_COLOR=$(color_for_usage_vs_pace "$usage_7d" "$target_7d")
reset_7d_label_str=""
[ -n "$resets_7d_label" ] && reset_7d_label_str="(${resets_7d_label}) "
pace_part_7d=""
[ -n "$target_7d" ] && pace_part_7d="\033[38;5;199m/${target_7d}"
[ -n "$usage_parts" ] && usage_parts="${usage_parts}\033[2m │ \033[0m"
usage_parts="${usage_parts}${U7_COLOR}wk ${reset_7d_label_str}${usage_7d}${pace_part_7d}%\033[0m"
fi
# Assemble the status line
line="\033[38;2;87;199;255m${dir_display}\033[0m${git_color}${git_info}\033[0m\033[2m │ ${CTX_COLOR}ctx ${context_pct}%\033[0m"
if [ -n "$usage_parts" ]; then
line="${line}\033[2m │ \033[0m${usage_parts}"
fi
line="${line}\033[2m │ ${model_name}\033[0m"
printf "%b\n" "$line"
@jtbr
Copy link
Copy Markdown

jtbr commented Feb 18, 2026

Great work. These are replacement lines to make it work in Linux (and in theory WSL):

creds=$(<~/.claude/.credentials.json)

# was: stat -f%m "$USAGE_CACHE"
stat -c%Y "$USAGE_CACHE"

reset_epoch=$(date -ud $resets_5h +%s 2>/dev/null)
reset_epoch=$(date -ud $resets_7d +%s 2>/dev/null)

resets_5h_label=$(date -d "@$reset_epoch" '+%-l%p' | tr '[:upper:]' '[:lower:]' | tr -d ' ')
resets_7h_label=$(date -d "@$reset_epoch" '+%-l%p' | tr '[:upper:]' '[:lower:]' | tr -d ' ')

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