Skip to content

Instantly share code, notes, and snippets.

@rahulmax
Created July 3, 2026 18:20
Show Gist options
  • Select an option

  • Save rahulmax/848bb2d4ad85968f83461113c9a06467 to your computer and use it in GitHub Desktop.

Select an option

Save rahulmax/848bb2d4ad85968f83461113c9a06467 to your computer and use it in GitHub Desktop.
Claude Code statusline: 2-row grid with context + cache usage bars

Claude Code statusline — context + cache bars

A 2-row statusline for Claude Code: identity on the left, right-aligned usage graphs.

~/Projects/myapp   ↳ main                        CONTX ▬▬▬▬──────────── 12% 122k/1M
Opus ·1M HIGH  $4.20 $0.31  →1:29H ▬▬────        CACHE ▬▬▬▬▬▬▬▬▬▬▬───── 93% HIT
  • Row 1: working directory, git branch · context bar segmented by system/memory/messages
  • Row 2: model, effort, flags, session + turn cost, 5-hour rate limit · cache bar (read/created/fresh) with hit rate

Install

  1. Save statusline-command.sh to ~/.claude/statusline-command.sh
  2. Add to ~/.claude/settings.json:
"statusLine": {
  "type": "command",
  "command": "sh ~/.claude/statusline-command.sh",
  "padding": 0,
  "refreshInterval": 1
}

padding: 0 matters — the right-alignment math assumes it.

Requirements & caveats

  • jq required (brew install jq / apt install jq)
  • macOS/BSD as written — line with stat -f %m needs stat -c %Y on Linux
  • Pricing is hardcoded for current Opus/Sonnet/Haiku rates — adjust the P_IN/P_OUT/P_CW/P_CR table if rates change
  • Context segments are estimated — the statusline JSON doesn't expose per-category token sizes; tune SYS_BASE, MCP_PER, PLUG_PER to your setup
  • Bar glyphs (▬, ─, ↳) assume a font where they render width-1 (true of most modern monospace fonts)
#!/bin/sh
# Claude Code statusline — 2-row grid: identity left, usage graphs right-aligned.
# Layout (2 lines, right block flush to terminal edge via $COLUMNS):
# row1 ~/path/to/cwd ↳ branch CONTX <bar> used% used/win
# row2 model ·1M EFFORT flags MCP×N $total $turn →1:29H <bar> CACHE <bar> hit% HIT
#
# The context breakdown (sys / mem / msg) is ESTIMATED — the statusline JSON does
# not expose exact per-category token sizes. Tune the constants below to taste.
# All glyphs are width-1 (box-drawing + BMP punctuation), so ${#...} == rendered
# width and the right-alignment math needs no per-glyph correction.
input=$(cat)
# Count characters (not bytes) in ${#...} and keep '.' decimals in awk.
export LANG="${LANG:-en_US.UTF-8}"
# ---- tunable estimation constants (approximate) -------------------------------
SYS_BASE=11000 # base system prompt + built-in tools
MCP_PER=1800 # est. tokens added per connected MCP server
PLUG_PER=1200 # est. tokens added per enabled plugin
BAR_WIDTH=36 # cells wide for the context graph (~window-scaled)
MIN_USED=1 # min full cells for the used portion when nonzero
UCH="▬" # used cell glyph (full — centered thick rectangle)
FCH="─" # free cell glyph (light horizontal rule)
# ---- colors (256) -------------------------------------------------------------
RESET="\033[0m"; BOLD="\033[1m"
DIM="\033[38;5;240m"
C_MODEL="\033[1;38;5;255m"
C_TAG="\033[38;5;244m"
C_EFFORT="\033[38;5;221m"
C_MCP="\033[38;5;44m"
C_COST="\033[38;5;114m"
C_ADD="\033[38;5;114m"
C_DEL="\033[38;5;203m"
C_CWD="\033[38;5;75m"
C_GIT="\033[38;5;180m"
C_FAST="\033[38;5;208m"
C_SYS="\033[38;5;245m" # gray — system + tools + mcp
C_MEM="\033[38;5;141m" # purple — memory / CLAUDE.md
C_MSG="\033[38;5;114m" # green — conversation / messages
C_FREE="\033[38;5;238m" # dim — free headroom
C_READ="\033[38;5;114m" # green — cache read (warm / reused)
C_NEW="\033[38;5;196m" # red — cache creation (written this turn)
C_FRESH="\033[38;5;208m" # deep orange — fresh input (uncached)
C_RLABEL="\033[38;5;244m" # dim — row labels
C_VAL="\033[38;5;253m" # bright — metric values
# pressure color from a percent (0-100)
pcolor() {
p="$1"
if [ "$p" -ge 85 ]; then printf "\033[38;5;196m"
elif [ "$p" -ge 70 ]; then printf "\033[38;5;208m"
elif [ "$p" -ge 50 ]; then printf "\033[38;5;221m"
elif [ "$p" -ge 30 ]; then printf "\033[38;5;155m"
else printf "\033[38;5;114m"; fi
}
# ---- parse stdin (single jq pass, @sh-quoted for safe eval) -------------------
eval "$(printf '%s' "$input" | jq -r '@sh "
CWD=\(.cwd // "")
TRANSCRIPT=\(.transcript_path // "")
MODEL=\(.model.display_name // "")
EFFORT=\(.effort.level // "")
FAST=\(.fast_mode // false)
THINK=\(.thinking.enabled // false)
WIN=\(.context_window.context_window_size // 0)
USEDPCT=\(.context_window.used_percentage // 0)
TOK_IN=\(.context_window.current_usage.input_tokens // 0)
TOK_CC=\(.context_window.current_usage.cache_creation_input_tokens // 0)
TOK_CR=\(.context_window.current_usage.cache_read_input_tokens // 0)
TOK_OUT=\(.context_window.current_usage.output_tokens // 0)
COST=\(.cost.total_cost_usd // 0)
ADD=\(.cost.total_lines_added // 0)
DEL=\(.cost.total_lines_removed // 0)
RL5=\(.rate_limits.five_hour.used_percentage // -1)
RL5RESET=\(.rate_limits.five_hour.resets_at // 0)
RL7=\(.rate_limits.seven_day.used_percentage // -1)
"')"
[ -z "$WIN" ] && WIN=0
USED=$(( TOK_IN + TOK_CC + TOK_CR ))
RL5=$(printf '%.0f' "$RL5" 2>/dev/null); [ -z "$RL5" ] && RL5=-1
# Fallback: after /compact stdin can go stale; recover used tokens from transcript.
if [ "$USED" -le 0 ] && [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ]; then
USED=$(tail -n 200 "$TRANSCRIPT" 2>/dev/null | jq -r -n '
reduce (inputs | select(.type=="assistant" and (.message.usage//null)!=null)) as $x (null; $x)
| if . == null then 0 else
((.message.usage.input_tokens//0)+(.message.usage.cache_creation_input_tokens//0)+(.message.usage.cache_read_input_tokens//0))
end' 2>/dev/null)
[ -z "$USED" ] && USED=0
fi
# ---- slow-changing values (memory tokens, mcp + plugin counts), cached --------
CACHE="$HOME/.claude/cache/statusline.cache"
sig="$CWD:$(stat -f %m "$HOME/.claude.json" 2>/dev/null):$(stat -f %m "$HOME/.claude/settings.json" 2>/dev/null):$(stat -f %m "$HOME/.claude/CLAUDE.md" 2>/dev/null)"
cached=""; [ -f "$CACHE" ] && cached=$(cat "$CACHE" 2>/dev/null)
csig=${cached%% *}
if [ "$csig" = "$sig" ]; then
rest=${cached#* }
MEM_TOK=${rest%% *}; rest=${rest#* }
MCP=${rest%% *}; PLUG=${rest##* }
else
enc=$(printf '%s' "$CWD" | sed 's#/#-#g')
chars=0
for f in "$HOME/.claude/CLAUDE.md" "$CWD/CLAUDE.md" "$CWD/.claude/CLAUDE.md" "$HOME/.claude/projects/$enc/memory/MEMORY.md"; do
[ -f "$f" ] && chars=$(( chars + $(wc -c < "$f" 2>/dev/null || echo 0) ))
done
MEM_TOK=$(( chars / 4 ))
mcpg=$(jq '.mcpServers // {} | length' "$HOME/.claude.json" 2>/dev/null || echo 0)
mcpp=$(jq --arg d "$CWD" '.projects[$d].mcpServers // {} | length' "$HOME/.claude.json" 2>/dev/null || echo 0)
MCP=$(( mcpg + mcpp ))
PLUG=$(jq '[(.enabledPlugins // {}) | to_entries[] | select(.value==true)] | length' "$HOME/.claude/settings.json" 2>/dev/null || echo 0)
printf '%s\t%s\t%s\t%s' "$sig" "$MEM_TOK" "$MCP" "$PLUG" > "$CACHE" 2>/dev/null
fi
[ -z "$MEM_TOK" ] && MEM_TOK=0; [ -z "$MCP" ] && MCP=0; [ -z "$PLUG" ] && PLUG=0
# ---- derive segment token sizes ----------------------------------------------
SYS_TOK=$(( SYS_BASE + MCP * MCP_PER + PLUG * PLUG_PER ))
if [ "$USED" -lt $(( SYS_TOK + MEM_TOK )) ]; then
# early session: framework overhead dominates — cap to what's actually used
s=$SYS_TOK; [ "$s" -gt "$USED" ] && s=$USED
rem=$(( USED - s )); m=$MEM_TOK; [ "$m" -gt "$rem" ] && m=$rem
SYS_SHOW=$s; MEM_SHOW=$m; MSG_SHOW=$(( USED - s - m ))
else
SYS_SHOW=$SYS_TOK; MEM_SHOW=$MEM_TOK; MSG_SHOW=$(( USED - SYS_TOK - MEM_TOK ))
fi
[ "$MSG_SHOW" -lt 0 ] && MSG_SHOW=0
FREE_TOK=$(( WIN - USED )); [ "$FREE_TOK" -lt 0 ] && FREE_TOK=0
upct_int=$(printf '%.0f' "$USEDPCT" 2>/dev/null || echo 0)
MSG_COLOR=$(pcolor "$upct_int")
# ---- helpers ------------------------------------------------------------------
kfmt() { awk -v n="$1" 'BEGIN{
if(n>=1000000){v=n/1000000; if(v==int(v))printf "%dM",v; else printf "%.1fM",v}
else if(n>=1000)printf "%dk",(n/1000+0.5);
else printf "%d",n }'; }
# small pressure-colored bar for a percent. minibar <pct> <width>
minibar() {
awk -v p="$1" -v w="$2" -v col="$(pcolor "$1")" -v free="$C_FREE" \
-v rst="$RESET" -v uch="$UCH" -v fch="$FCH" 'BEGIN{
f=int(p/100*w+0.5); if(f>w)f=w; if(p>0&&f<1)f=1; e=w-f
s=col; for(i=0;i<f;i++)s=s uch; s=s rst free; for(i=0;i<e;i++)s=s fch
printf "%s%s", s, rst }'
}
# ---- one window-scaled segmented row: 3 colored segments + free ---------------
# make_row <seg1> <seg2> <seg3> <col1> <col2> <col3> [overlay] [overlay_len]
# overlay: pre-colored text drawn inside the free zone; overlay_len = its visible width
make_row() {
awk -v W="$BAR_WIDTH" -v MIN="$MIN_USED" -v win="$WIN" -v used="$USED" \
-v s1="$1" -v s2="$2" -v s3="$3" -v c1="$4" -v c2="$5" -v c3="$6" \
-v ov="$7" -v ovlen="$8" \
-v cfree="$C_FREE" -v rst="$RESET" -v uch="$UCH" -v fch="$FCH" '
function rep(n,ch, s,i){s="";for(i=0;i<n;i++)s=s ch;return s}
BEGIN{
if(win<=0)win=1
ucf=used/win*W # fractional used cells
uc=int(ucf+0.5) # round to nearest whole cell
if(used>0 && uc<MIN)uc=MIN
if(uc>W)uc=W
if(used>0){h1=s1/used;h2=s2/used;h3=s3/used}else{h1=0;h2=0;h3=0}
p1=int(h1*uc+0.5); p2=int((h1+h2)*uc+0.5)
a=p1; b=p2-p1; c=uc-p2
if(s1>0 && h1>=0.05 && a<1)a=1
if(s2>0 && h2>=0.05 && b<1)b=1
if(s3>0 && h3>=0.05 && c<1)c=1
sum=a+b+c
while(sum>uc){
if(a>=b && a>=c && a>1)a--
else if(b>=c && b>1)b--
else if(c>1)c--
else break
sum=a+b+c
}
cf=W-(a+b+c); if(cf<0)cf=0
out = c1 rep(a,uch) rst c2 rep(b,uch) rst c3 rep(c,uch) rst
if(ovlen>0){
pad = cf - ovlen # right-align overlay to the bar edge
if(pad>=0) out = out cfree rep(pad,fch) rst ov
else out = out " " ov # free zone too small: overflow with 1 gap
} else {
out = out cfree rep(cf,fch) rst
}
printf "%s", out
}'
}
# usage label embedded in the CTX bar: "12% 122k/1M"
usage_plain="${upct_int}% $(kfmt "$USED")/$(kfmt "$WIN")"
usage_in_bar=$(printf "%b%s%%%b %b%s/%s%b" \
"$MSG_COLOR" "$upct_int" "$RESET" "$C_VAL" "$(kfmt "$USED")" "$(kfmt "$WIN")" "$RESET")
# cache-hit label embedded in the CACHE bar: "93% HIT"
hit_in_bar=""; hit_plain=""
if [ "$USED" -gt 0 ]; then
hit=$(awk -v r="$TOK_CR" -v u="$USED" 'BEGIN{printf "%.0f", r/u*100}')
hit_plain="${hit}% HIT"
hit_in_bar=$(printf "%b%s%% HIT%b" "$C_READ" "$hit" "$RESET")
fi
# ---- LEFT row 1: working directory + branch -----------------------------------
# Every glyph below is width-1, so ${#L*_plain} (LANG=UTF-8) is the rendered width.
short_cwd=$(printf '%s' "${CWD:-$PWD}" | sed "s|^$HOME|~|")
branch=$(git -C "${CWD:-$PWD}" symbolic-ref --short HEAD 2>/dev/null || git -C "${CWD:-$PWD}" rev-parse --short HEAD 2>/dev/null)
L1=$(printf "%b%s%b" "$C_CWD" "$short_cwd" "$RESET")
L1_plain="$short_cwd"
if [ -n "$branch" ]; then
L1=$(printf "%b %b↳ %s%b" "$L1" "$C_GIT" "$branch" "$RESET")
L1_plain="$L1_plain ↳ $branch"
fi
# ---- LEFT row 2: model · effort · flags · mcp · $ · 5h ------------------------
model_base=${MODEL%% (*}
L2=$(printf "%b%s%b" "$C_MODEL" "$model_base" "$RESET"); L2_plain="$model_base"
case "$MODEL" in *1M*) L2=$(printf "%b %b·1M%b" "$L2" "$C_TAG" "$RESET"); L2_plain="$L2_plain ·1M";; esac
if [ -n "$EFFORT" ]; then
EFFORT_UP=$(printf '%s' "$EFFORT" | tr '[:lower:]' '[:upper:]')
L2=$(printf "%b %b%s%b" "$L2" "$C_EFFORT" "$EFFORT_UP" "$RESET"); L2_plain="$L2_plain $EFFORT_UP"
fi
[ "$THINK" = "true" ] && { L2=$(printf "%b %bTHINK%b" "$L2" "$C_TAG" "$RESET"); L2_plain="$L2_plain THINK"; }
[ "$FAST" = "true" ] && { L2=$(printf "%b %bFAST%b" "$L2" "$C_FAST" "$RESET"); L2_plain="$L2_plain FAST"; }
cost_str=$(awk -v c="$COST" 'BEGIN{printf "%.2f",c}')
# current-turn cost: price the live context usage at the model's per-MTok rates.
# Cache creation uses the 1h rate (2x input) — Claude Code writes the 1h cache.
# No >200k long-context premium on Opus 4.8 / Sonnet 4.6, so rates are flat.
case "$MODEL" in
*Haiku*) P_IN=1; P_OUT=5; P_CW=2; P_CR=0.1 ;;
*Sonnet*) P_IN=3; P_OUT=15; P_CW=6; P_CR=0.3 ;;
*) P_IN=5; P_OUT=25; P_CW=10; P_CR=0.5 ;; # Opus (default)
esac
turn=$(awk -v i="$TOK_IN" -v cc="$TOK_CC" -v cr="$TOK_CR" -v o="$TOK_OUT" \
-v pi="$P_IN" -v pcw="$P_CW" -v pcr="$P_CR" -v po="$P_OUT" \
'BEGIN{printf "%.2f", (i*pi + cc*pcw + cr*pcr + o*po)/1000000}')
L2=$(printf "%b %b\$%s%b %b\$%s%b" "$L2" "$C_COST" "$cost_str" "$RESET" "$C_TAG" "$turn" "$RESET")
L2_plain="$L2_plain \$$cost_str \$$turn"
if [ "$RL5" -ge 0 ] 2>/dev/null; then
rl5c=$(pcolor "$RL5")
RL5LBL="5H"
if [ "$RL5RESET" -gt 0 ] 2>/dev/null; then
rem=$(( RL5RESET - $(date +%s) )); [ "$rem" -lt 0 ] && rem=0
RL5LBL=$(printf '\xe2\x86\x92%d:%02dH' $(( rem / 3600 )) $(( rem % 3600 / 60 )))
fi
L2=$(printf "%b %b%s%b %s %b%s%%%b" "$L2" "$C_RLABEL" "$RL5LBL" "$RESET" "$(minibar "$RL5" 6)" "$rl5c" "$RL5" "$RESET")
L2_plain="$L2_plain $RL5LBL ______ ${RL5}%" # ______ = 6 minibar cells (width placeholder)
fi
# ---- size bars from the ACTUAL left width so numbers never truncate -----------
# The right block worst case is "CONTX " (6) + bar + 1 + overlay (the overlay can
# spill past the bar at high usage). Reserve all of it, plus MARGIN at the rim so
# the host's own padding can never clip the trailing tokens.
: "${COLUMNS:=120}"
# Claude Code indents the statusline and reserves a few columns, so the usable
# width is < $COLUMNS. Keep this many cells clear of the right edge or the
# trailing CONTX/CACHE numbers get clipped with "…". Raise if still clipped.
MARGIN=8
GAP=2 # minimum space between the left and right blocks
edge=$(( COLUMNS - MARGIN ))
LW1=${#L1_plain}; LW2=${#L2_plain}
maxLW=$LW1; [ "$LW2" -gt "$maxLW" ] && maxLW=$LW2
op=${#usage_plain}; oc=${#hit_plain}
ovmax=$op; [ "$oc" -gt "$ovmax" ] && ovmax=$oc
BAR_WIDTH=$(( edge - maxLW - GAP - 6 - 1 - ovmax ))
[ "$BAR_WIDTH" -gt 36 ] && BAR_WIDTH=36
[ "$BAR_WIDTH" -lt 10 ] && BAR_WIDTH=10
ROW_CTX=$(make_row "$SYS_SHOW" "$MEM_SHOW" "$MSG_SHOW" "$C_SYS" "$C_MEM" "$C_MSG" "$usage_in_bar" "$op")
ROW_CACHE=$(make_row "$TOK_CR" "$TOK_CC" "$TOK_IN" "$C_READ" "$C_NEW" "$C_FRESH" "$hit_in_bar" "$oc")
# ---- RIGHT column: CONTX (row1) + CACHE (row2), flush right -------------------
lbl_ctx=$(printf "%bCONTX%b" "$C_RLABEL" "$RESET")
lbl_cache=$(printf "%bCACHE%b" "$C_RLABEL" "$RESET")
RIGHT1=$(printf "%b %s" "$lbl_ctx" "$ROW_CTX")
RIGHT2=$(printf "%b %s" "$lbl_cache" "$ROW_CACHE")
# measure the right blocks exactly (every glyph is width-1) — no estimation error
ESC=$(printf '\033')
vwidth() { printf '%s' "$1" | sed "s/${ESC}\[[0-9;]*m//g" | LC_CTYPE=UTF-8 wc -m | tr -d ' '; }
RW1=$(vwidth "$RIGHT1"); RW2=$(vwidth "$RIGHT2")
# pad so each right block ends at `edge`, leaving MARGIN cells at the rim
pad1=$(( edge - LW1 - RW1 )); [ "$pad1" -lt "$GAP" ] && pad1=$GAP
pad2=$(( edge - LW2 - RW2 )); [ "$pad2" -lt "$GAP" ] && pad2=$GAP
SP1=$(printf "%*s" "$pad1" "")
SP2=$(printf "%*s" "$pad2" "")
printf "%b%s%b\n%b%s%b\n" "$L1" "$SP1" "$RIGHT1" "$L2" "$SP2" "$RIGHT2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment