My Claude Code Status Line: Usage, Pace, and Context at a Glance
I've been using Claude Code as my daily driver for a while now. One thing that kept bugging me was not knowing where I stood with my usage limits until I actually hit them. Claude Code has a status line feature that lets you run a custom script and display whatever you want at the bottom of the terminal. I built one that shows me everything I care about in a single line.
Freek Van der Herten wrote a nice post about his own status line showing the repo name and context window percentage. That was my starting point, but I wanted more: rate limit tracking, reset countdowns, and a pace indicator that tells me whether I'm burning through my weekly budget too fast.
What It Looks Like

That bottom line is the status bar. Here's what each section means, left to right:
laravel-modules:mainis the current repo and branch. Useful when you're jumping between projects.Opus 4.6 (1M context)is the active model. I like knowing exactly which model I'm talking to.5h 5% (2h 16m)is my 5-hour rolling usage at 5%, resetting in 2 hours and 16 minutes.7d 2% (5d 1h)is my 7-day usage at 2%, resetting in 5 days and 1 hour.pace +26%is the interesting one. It means I'm 26% under my daily budget for the week. Green means I'm pacing well. Red would mean I'm ahead of schedule and might hit the weekly cap.
Colors follow a traffic light pattern: green when usage is below 50%, yellow between 50% and 79%, red at 80% and above.
How the Pace Indicator Works
This was the part I was most happy with. The idea is simple: if you have a 7-day budget, you should ideally use about 1/7th of it per day. The script figures out which day of the current cycle you're on, calculates what percentage you "should" have used by now, and compares it to your actual usage.
So if it's day 3 of 7, your budget is roughly 43% (3/7). If your actual usage is 17%, you're 26% under budget. That shows up as pace +26% in green. If you were at 60% on day 3, you'd see pace -17% in red, which is a good signal to ease off before hitting the weekly limit.
The Script
Drop this into ~/.claude/statusline.sh:
#!/bin/bash
input=$(cat)
model=$(echo "$input" | jq -r '.model.display_name // empty' 2>/dev/null || echo "$input" | grep -oP '"display_name":\s*"\K[^"]+' 2>/dev/null | head -1)
repo=$(git remote get-url origin 2>/dev/null | sed 's/.*[:\/]\([^/]*\)\.git$/\1/' | sed 's/.*[:\/]\([^/]*\)$/\1/')
branch=$(git branch --show-current 2>/dev/null || echo 'no-git')
SEP=" · "
RESET="\033[0m"
BOLD="\033[1m"
DIM="\033[2m"
CYAN="\033[36m"
GREEN="\033[32m"
YELLOW="\033[33m"
RED="\033[31m"
MAGENTA="\033[35m"
BLUE="\033[34m"
# Cross-platform helpers: GNU (Linux/Windows Git Bash) then BSD (macOS)
parse_iso_date() {
date -d "$1" +%s 2>/dev/null || \
date -jf "%Y-%m-%dT%H:%M:%S" "$(echo "$1" | sed 's/\.[0-9]*Z$//' | sed 's/Z$//')" +%s 2>/dev/null
}
file_mtime() {
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || echo 0
}
json_val() {
jq -r "$1" "$2" 2>/dev/null || grep -oP "$3" "$2" 2>/dev/null
}
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty' 2>/dev/null || echo "$input" | grep -oP '"used_percentage":\K[0-9]+' 2>/dev/null | head -1)
ctx_pct=${ctx_pct%.*}
CACHE_FILE="$HOME/.claude/.usage-cache"
CACHE_MAX_AGE=120
now=$(date +%s)
should_refresh=false
if [ ! -f "$CACHE_FILE" ]; then
should_refresh=true
else
cache_mtime=$(file_mtime "$CACHE_FILE")
if [ $((now - cache_mtime)) -ge $CACHE_MAX_AGE ]; then
should_refresh=true
fi
fi
if [ "$should_refresh" = true ]; then
ACCESS_TOKEN=$(jq -r '.claudeAiOauth.accessToken // .accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null || grep -oP '"accessToken":"\K[^"]+' "$HOME/.claude/.credentials.json" 2>/dev/null | head -1)
if [ -n "$ACCESS_TOKEN" ]; then
usage=$(curl -s --max-time 3 "https://api.anthropic.com/api/oauth/usage" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "User-Agent: claude-code/2.1.42" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "anthropic-beta: oauth-2025-04-20" 2>/dev/null)
if echo "$usage" | jq -e '.five_hour' >/dev/null 2>&1 || echo "$usage" | grep -q "five_hour" 2>/dev/null; then
echo "$usage" > "$CACHE_FILE"
elif [ -f "$CACHE_FILE" ]; then
touch "$CACHE_FILE"
fi
fi
fi
five_hour=""
seven_day=""
five_reset=""
seven_reset=""
if [ -f "$CACHE_FILE" ]; then
five_hour=$(json_val '.five_hour.utilization // empty' "$CACHE_FILE" '"five_hour":\{"utilization":\K[0-9.]+')
seven_day=$(json_val '.seven_day.utilization // empty' "$CACHE_FILE" '"seven_day":\{"utilization":\K[0-9.]+')
five_reset_raw=$(json_val '.five_hour.resets_at // empty' "$CACHE_FILE" '"five_hour":\{"utilization":[0-9.]+,"resets_at":"\K[^"]+')
seven_reset_raw=$(json_val '.seven_day.resets_at // empty' "$CACHE_FILE" '"seven_day":\{"utilization":[0-9.]+,"resets_at":"\K[^"]+')
# Invalidate cached values if the reset time has already passed.
# After a reset, the old utilization % is guaranteed wrong — the window started over.
is_past_reset() {
local reset_epoch
reset_epoch=$(parse_iso_date "$1")
[ -n "$reset_epoch" ] && [ "$now" -ge "$reset_epoch" ]
}
if [ -n "$five_reset_raw" ] && is_past_reset "$five_reset_raw"; then
five_hour=""
five_reset_raw=""
fi
if [ -n "$seven_reset_raw" ] && is_past_reset "$seven_reset_raw"; then
seven_day=""
seven_reset_raw=""
fi
# Also mark stale if cache is older than 5 minutes (API may be failing silently)
STALE_THRESHOLD=300
cache_age=$((now - $(file_mtime "$CACHE_FILE")))
usage_stale=false
if [ "$cache_age" -ge "$STALE_THRESHOLD" ]; then
usage_stale=true
fi
time_until() {
local reset_epoch
reset_epoch=$(parse_iso_date "$1")
if [ -z "$reset_epoch" ]; then return; fi
local diff=$(( reset_epoch - now ))
if [ "$diff" -le 0 ]; then echo "now"; return; fi
local hours=$(( diff / 3600 ))
local mins=$(( (diff % 3600) / 60 ))
if [ "$hours" -gt 24 ]; then
local days=$(( hours / 24 ))
hours=$(( hours % 24 ))
echo "${days}d ${hours}h"
elif [ "$hours" -gt 0 ]; then
echo "${hours}h ${mins}m"
else
echo "${mins}m"
fi
}
five_reset=$(time_until "$five_reset_raw")
seven_reset=$(time_until "$seven_reset_raw")
if [ -n "$seven_reset_raw" ] && [ -n "$seven_day" ]; then
seven_reset_epoch=$(parse_iso_date "$seven_reset_raw")
if [ -n "$seven_reset_epoch" ]; then
remaining_secs=$(( seven_reset_epoch - now ))
if [ "$remaining_secs" -gt 0 ] && [ "$remaining_secs" -lt 604800 ]; then
elapsed_secs=$(( 604800 - remaining_secs ))
current_day=$(( elapsed_secs / 86400 + 1 ))
[ "$current_day" -gt 7 ] && current_day=7
budget_pct=$(( current_day * 100 / 7 ))
budget_diff=$(( budget_pct - ${seven_day%.*} ))
if [ "$budget_diff" -ge 0 ]; then
pace_indicator="${GREEN}+${budget_diff}%${RESET}"
else
pace_indicator="${RED}${budget_diff}%${RESET}"
fi
fi
fi
fi
fi
five_hour=${five_hour%.*}
seven_day=${seven_day%.*}
color_for_pct() {
local pct=${1:-0}
if [ "$pct" -ge 80 ]; then
echo -e "$RED"
elif [ "$pct" -ge 50 ]; then
echo -e "$YELLOW"
else
echo -e "$GREEN"
fi
}
output=""
if [ -n "$repo" ]; then
output+="${BLUE}${repo}${DIM}:${CYAN}${branch}${RESET}"
output+="${DIM}${SEP}${RESET}"
fi
output+="${BOLD}${MAGENTA}${model:-Claude}${RESET}"
if [ -n "$five_hour" ] && [ -n "$seven_day" ]; then
stale_mark=""
[ "$usage_stale" = true ] && stale_mark="${DIM}?${RESET}"
five_color=$(color_for_pct "${five_hour:-0}")
seven_color=$(color_for_pct "${seven_day:-0}")
output+="${DIM}${SEP}${RESET}"
output+="${DIM}5h${RESET} ${five_color}${BOLD}${five_hour:-0}%${stale_mark}${RESET}"
[ -n "$five_reset" ] && output+=" ${CYAN}(${five_reset})${RESET}"
output+="${DIM}${SEP}${RESET}"
output+="${DIM}7d${RESET} ${seven_color}${BOLD}${seven_day:-0}%${stale_mark}${RESET}"
[ -n "$seven_reset" ] && output+=" ${CYAN}(${seven_reset})${RESET}"
[ -n "$pace_indicator" ] && output+="${DIM}${SEP}${RESET}${DIM}pace${RESET} ${pace_indicator}"
fi
if [ -n "$ctx_pct" ] && [ "$ctx_pct" != "null" ]; then
ctx_color=$(color_for_pct "${ctx_pct:-0}")
output+="${DIM}${SEP}${RESET}"
output+="${DIM}ctx${RESET} ${ctx_color}${BOLD}${ctx_pct}%${RESET}"
fi
printf "%b" "$output"
echo
It's about 170 lines. Not tiny, but not bloated either. Every line does something.
What's Going On in There
The script reads JSON from stdin (Claude Code pipes session data to it), pulls out the model name and context window usage, then does a few things on its own:
Usage data from the API. It reads your OAuth access token from ~/.claude/.credentials.json and hits Anthropic's usage endpoint to get your 5-hour and 7-day utilization percentages. To keep things responsive, it caches the response for 2 minutes in ~/.claude/.usage-cache. The status line script runs on every render, so without caching you'd be making API calls constantly.
Reset countdowns. The API returns ISO 8601 timestamps for when each limit resets. The time_until function converts those into human-readable countdowns like 2h 16m or 5d 1h.
Pace calculation. This divides the 7-day cycle into daily budgets. If you're on day 3, your "expected" usage is 3/7ths of the total. The difference between expected and actual shows whether you're pacing well (green positive number) or burning through your budget too quickly (red negative number).
Cross-platform helpers. The three small functions at the top (parse_iso_date, file_mtime, json_val) handle the GNU vs BSD tool differences between Linux/Windows and macOS. Each one tries the GNU syntax first and falls back to the BSD equivalent. On Windows with Git Bash, the GNU versions work. On macOS, the BSD fallbacks kick in. No if blocks checking uname, just quiet fallthrough.
Setting It Up
Two steps.
1. Create the script file. Save the script above to ~/.claude/statusline.sh and make it executable:
chmod +x ~/.claude/statusline.sh
On Windows with Git Bash, chmod isn't strictly necessary, but it doesn't hurt.
2. Add it to your Claude Code settings. Open ~/.claude/settings.json (create it if it doesn't exist) and add the statusLine key:
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh"
}
}
That's it. Next time you start a Claude Code session, the status line appears at the bottom.
Prerequisites
The script needs curl to fetch usage data from the API. You almost certainly have this already.
For JSON parsing, it tries jq first and falls back to grep with Perl regex. If you have jq installed, great. If you don't (Windows Git Bash often doesn't ship with it), the grep -oP fallback handles it. On macOS where grep -oP doesn't work, jq is your best bet. Install it with brew install jq if you don't have it.
What If You Don't Want All of It
The output section at the bottom of the script is where everything comes together. You can comment out or remove any block you don't need. Want just the repo and model without usage tracking? Delete everything from the CACHE_FILE line down to the end of the fi block, and remove the usage section in the output builder. The sections are independent enough that you can pick what matters to you.
This is a script on your machine, not a package. Make it yours.
Stay in the Loop
Get the latest posts delivered to your inbox - on your schedule.