Skip to content

Instantly share code, notes, and snippets.

@pratos
Last active October 28, 2026 10:22
Show Gist options
  • Select an option

  • Save pratos/15bdb85c53ed381eacd878722da9ead5 to your computer and use it in GitHub Desktop.

Select an option

Save pratos/15bdb85c53ed381eacd878722da9ead5 to your computer and use it in GitHub Desktop.
Claude Code status line: context and 5h/7d rate-limit meters

Claude Code status line

A bash status line for Claude Code: host, path and git status, then model, effort, a context meter with tokens used out of the window size (e.g. 158k/200k), and the clock, then 5-hour and 7-day rate-limit meters with time until reset. Bars go green, yellow, then red at 50% and 80%.

Needs jq.

  1. Save statusline-command.sh to ~/.claude/statusline-command.sh.
  2. Add this to ~/.claude/settings.json:
"statusLine": {
  "type": "command",
  "command": "bash ~/.claude/statusline-command.sh"
}
#!/usr/bin/env bash
#
# Claude Code status line.
#
# Claude Code runs this script on every redraw and pipes session data to it
# as JSON on stdin. Whatever the script prints becomes the status line:
#
# ┌─host as user in ../path/to/repo on main ✔ node v22.0.0
# ├─Opus effort high ctx ▰▰▰▰▱▱▱▱▱▱ 41% 82k/200k 21:03:22
# └─5h ▰▰▰▰▰▰▰▰▰▱ 92% ↺ 1h32m 7d ▰▰▰▰▰▰▰▰▰▱ 93% ↺ 2d4h
#
# Setup: save as ~/.claude/statusline-command.sh, then add to
# ~/.claude/settings.json:
#
# "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" }
#
# Needs: jq. The first line is styled after a Starship prompt.
# ──────────────────────────────────────────────────────────────────────────
# Colours
# ──────────────────────────────────────────────────────────────────────────
RESET=$'\e[0m'
DIM=$'\e[2m'
RED=$'\e[31m'
GREEN=$'\e[1;32m'
YELLOW=$'\e[1;33m'
BLUE=$'\e[1;34m'
MAGENTA=$'\e[1;35m'
CYAN=$'\e[1;36m'
WHITE=$'\e[1;37m'
# Meter colours (not bold).
METER_OK=$'\e[32m' # under 50%
METER_WARN=$'\e[33m' # 50% and up
METER_HOT=$'\e[31m' # 80% and up
# ──────────────────────────────────────────────────────────────────────────
# Read the session JSON
# ──────────────────────────────────────────────────────────────────────────
input=$(cat)
# field '<jq path>' prints the value, or nothing when it's missing.
field() { echo "$input" | jq -r "$1 // empty"; }
cwd=$(field '.workspace.current_dir // .cwd')
model=$(field '.model.display_name')
effort=$(field '.effort.level')
context_used=$(field '.context_window.used_percentage')
context_size=$(field '.context_window.context_window_size')
# Tokens in the window: the last request's input, cache writes and cache reads.
# Before the first request there's no usage yet, so estimate from the percentage.
context_tokens=$(echo "$input" | jq -r '.context_window as $c
| if $c.current_usage then
($c.current_usage.input_tokens // 0)
+ ($c.current_usage.cache_creation_input_tokens // 0)
+ ($c.current_usage.cache_read_input_tokens // 0)
elif $c.used_percentage and $c.context_window_size then
($c.used_percentage * $c.context_window_size / 100 | round)
else empty end')
thinking_off=$(echo "$input" | jq -r 'if .thinking.enabled == false then "yes" else "" end')
five_hour_used=$(field '.rate_limits.five_hour.used_percentage')
five_hour_resets=$(field '.rate_limits.five_hour.resets_at') # epoch seconds
seven_day_used=$(field '.rate_limits.seven_day.used_percentage')
seven_day_resets=$(field '.rate_limits.seven_day.resets_at') # epoch seconds
# ──────────────────────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────────────────────
# short_path /Users/me/a/b/c/d → ../b/c/d
# Replaces $HOME with ~ and keeps only the last three folders.
short_path() {
local path="$1" parts n i out
case "$path" in "$HOME"*) path="~${path#$HOME}" ;; esac
IFS='/' read -r -a parts <<< "$path"
n=${#parts[@]}
if [ "$n" -le 3 ]; then
printf '%s' "$path"
return
fi
out="../"
for ((i = n - 3; i < n; i++)); do
out+="${parts[i]}"
[ "$i" -lt $((n - 1)) ] && out+="/"
done
printf '%s' "$out"
}
# meter 73 → ▰▰▰▰▰▰▰▱▱▱ 73%
# Ten cells, coloured green, then yellow from 50%, then red from 80%.
meter() {
local pct filled bar="" colour i
pct=$(printf '%.0f' "$1")
filled=$(( (pct * 10 + 50) / 100 )) # round to the nearest cell
[ "$filled" -gt 10 ] && filled=10
[ "$filled" -lt 0 ] && filled=0
for ((i = 0; i < 10; i++)); do
if [ "$i" -lt "$filled" ]; then bar+="▰"; else bar+="▱"; fi
done
if [ "$pct" -ge 80 ]; then colour=$METER_HOT
elif [ "$pct" -ge 50 ]; then colour=$METER_WARN
else colour=$METER_OK
fi
printf '%s' "${colour}${bar} ${pct}%${RESET}"
}
# tokens 158234 → "158k"; also "950" and "1.2M"
tokens() {
local t="$1"
if [ "$t" -ge 1000000 ]; then
if [ $(( t % 1000000 )) -lt 100000 ]; then printf '%dM' $(( t / 1000000 ))
else printf '%d.%dM' $(( t / 1000000 )) $(( t % 1000000 / 100000 ))
fi
elif [ "$t" -ge 1000 ]; then printf '%dk' $(( (t + 500) / 1000 ))
else printf '%d' "$t"
fi
}
# time_until <epoch> → "3d5h", "2h14m", "9m" or "now"
time_until() {
local left=$(( $1 - $(date +%s) ))
if [ "$left" -le 0 ]; then printf 'now'; return; fi
local days=$(( left / 86400 ))
local hours=$(( left % 86400 / 3600 ))
local mins=$(( left % 3600 / 60 ))
if [ "$days" -gt 0 ]; then printf '%dd%dh' "$days" "$hours"
elif [ "$hours" -gt 0 ]; then printf '%dh%02dm' "$hours" "$mins"
else printf '%dm' "$mins"
fi
}
# limit_meter <label> <used %> <resets at> → "5h ▰▰▰▱▱▱▱▱▱▱ 31% ↺ 2h14m"
limit_meter() {
local label="$1" used="$2" resets="$3"
printf '%s %s' "$label" "$(meter "$used")"
[ -n "$resets" ] && printf ' %s' "${DIM}↺ $(time_until "$resets")${RESET}"
}
# ──────────────────────────────────────────────────────────────────────────
# Git: branch, file status symbols, and any rebase/merge in progress
# ──────────────────────────────────────────────────────────────────────────
# Status symbols (same as Starship's defaults):
# xxx conflicts + staged ! modified >> renamed ✘ deleted
# ? untracked ≡ stashed ⇡ ahead ⇣ behind ⇕ diverged
# ✔ clean
git_segment() {
local dir="$1" porcelain header files branch symbols="" ahead behind git_dir state=""
git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 || return
# --no-optional-locks: this runs on every redraw, so never hold
# index.lock and block a git command running at the same time.
porcelain=$(git -C "$dir" --no-optional-locks status --porcelain=v1 -b 2>/dev/null)
header=$(echo "$porcelain" | head -1) # "## main...origin/main [ahead 1]"
files=$(echo "$porcelain" | tail -n +2)
branch=${header#\#\# }
branch=${branch%%...*}
branch=${branch%% \[*}
if [ "$branch" = "HEAD (no branch)" ]; then
branch=$(git -C "$dir" rev-parse --short HEAD 2>/dev/null)
fi
# has_status '<regex>' is true when any porcelain line matches.
has_status() { echo "$files" | grep -qE "$1"; }
has_status '^(UU|AA|DD|AU|UA|DU|UD)' && symbols+="xxx"
has_status '^[MARC]' && symbols+="+"
has_status '^.[MT]' && symbols+="!"
has_status '^(R.|.R)' && symbols+=">>"
has_status '^( D|D |.D)' && symbols+="✘"
has_status '^\?\?' && symbols+="?"
git -C "$dir" rev-parse --verify -q refs/stash >/dev/null 2>&1 && symbols+="≡"
ahead=$(echo "$header" | sed -n 's/.*ahead \([0-9]*\).*/\1/p')
behind=$(echo "$header" | sed -n 's/.*behind \([0-9]*\).*/\1/p')
if [ -n "$ahead" ] && [ -n "$behind" ]; then symbols+="⇕"
elif [ -n "$ahead" ]; then symbols+="⇡$ahead"
elif [ -n "$behind" ]; then symbols+="⇣$behind"
fi
[ -z "$symbols" ] && symbols="✔"
git_dir=$(git -C "$dir" rev-parse --git-dir 2>/dev/null)
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ]; then state="REBASING"
elif [ -f "$git_dir/MERGE_HEAD" ]; then state="MERGING"
elif [ -f "$git_dir/CHERRY_PICK_HEAD" ]; then state="CHERRY-PICKING"
fi
printf ' on %s' "${CYAN}${branch}${RESET}"
[ -n "$state" ] && printf '|%s' "${RED}${state}${RESET}"
printf ' %s' "${RED}${symbols}${RESET}"
}
# ──────────────────────────────────────────────────────────────────────────
# Languages: show a toolchain version when its project file is present
# ──────────────────────────────────────────────────────────────────────────
languages_segment() {
cd "$1" 2>/dev/null || return
# show <name and version>
show() { printf ' %s' "${BLUE}$1${RESET}"; }
has() { command -v "$1" >/dev/null; }
[ -f package.json ] && has node && show "node $(node --version)"
[ -f Cargo.toml ] && has rustc && show "rust v$(rustc --version | awk '{print $2}')"
[ -f go.mod ] && has go && show "go v$(go version | awk '{print $3}' | sed 's/go//')"
{ [ -f pyproject.toml ] || [ -f requirements.txt ]; } && has python3 \
&& show "py v$(python3 --version | awk '{print $2}')"
[ -f Gemfile ] && has ruby && show "ruby v$(ruby --version | awk '{print $2}')"
[ -f mix.exs ] && has elixir \
&& show "elixir v$(elixir --version 2>/dev/null | awk '/Elixir/{print $2}')"
}
# ──────────────────────────────────────────────────────────────────────────
# Line 1: where you are
# ──────────────────────────────────────────────────────────────────────────
line1="┌─${GREEN}$(hostname -s)${RESET}"
line1+=" as ${BLUE}$(whoami)${RESET}"
line1+=" in ${YELLOW}$(short_path "$cwd")${RESET}"
line1+="$(git_segment "$cwd")"
line1+="$(languages_segment "$cwd")"
# ──────────────────────────────────────────────────────────────────────────
# Line 3: rate limits (only on plans that report them)
# ──────────────────────────────────────────────────────────────────────────
limits=""
[ -n "$five_hour_used" ] && limits+="$(limit_meter 5h "$five_hour_used" "$five_hour_resets")"
if [ -n "$seven_day_used" ]; then
[ -n "$limits" ] && limits+=" "
limits+="$(limit_meter 7d "$seven_day_used" "$seven_day_resets")"
fi
# ──────────────────────────────────────────────────────────────────────────
# Line 2: the session. It closes the box (└─) when there's no limits line.
# ──────────────────────────────────────────────────────────────────────────
if [ -n "$limits" ]; then line2="├─"; else line2="└─"; fi
[ -n "$model" ] && line2+="${GREEN}${model}${RESET}"
if [ -n "$thinking_off" ]; then line2+=" ${DIM}thinking off${RESET}"
elif [ -n "$effort" ]; then line2+=" effort ${MAGENTA}${effort}${RESET}"
fi
if [ -n "$context_used" ]; then
line2+=" ctx $(meter "$context_used")"
if [ -n "$context_tokens" ]; then
size=""
[ -n "$context_size" ] && size="/$(tokens "$context_size")"
line2+=" ${DIM}$(tokens "$context_tokens")${size}${RESET}"
fi
fi
line2+=" ${WHITE}$(date +%T)${RESET}"
# ──────────────────────────────────────────────────────────────────────────
# Print
# ──────────────────────────────────────────────────────────────────────────
printf '%s\n' "$line1"
printf '%s' "$line2"
[ -n "$limits" ] && printf '\n└─%s' "$limits"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment