Skip to content

Instantly share code, notes, and snippets.

@ekinertac
Created July 22, 2026 14:08
Show Gist options
  • Select an option

  • Save ekinertac/4b7ebb97039bfdec2ab9669e4bf51460 to your computer and use it in GitHub Desktop.

Select an option

Save ekinertac/4b7ebb97039bfdec2ab9669e4bf51460 to your computer and use it in GitHub Desktop.
Two-line Claude Code statusline: context bar, cost, duration, rate limits, claude-code-router aware

Two-line Claude Code statusline

[Opus 4.8] 📁 ~/Code/qwok | 🌿 master
███░░░░░░░ 31% | $2.14 | ⏱ 4m 07s | ⏱ 22/07 14:31:08 | 5h:12% 7d:41%

Line 1 is identity and location. Line 2 is pressure and economics: context bar (green under 70%, yellow to 90%, red above), session cost, session duration, the time of the last render, and subscription rate-limit usage.

Install

  1. Save statusline.sh to ~/.claude/statusline.sh
  2. chmod +x ~/.claude/statusline.sh
  3. Add this to ~/.claude/settings.json:
{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 1
  }
}

padding: 1 keeps one space of left margin. Set it to 0 to render flush against the terminal edge.

Requires jq. Without it the script prints [statusline: install jq] instead of failing silently.

Notes

  • No git dirty-check on purpose. It only runs git branch --show-current, so it stays cheap on large repos. The statusline fires after every assistant message.
  • The rate-limit segment is dropped entirely when the API doesn't return one (non-subscription usage).
  • The wall-clock field carries the date (22/07 14:31:08) because the script never polls. A session left idle for two days keeps showing its last render time, and the date makes that obvious instead of looking recent.
  • stat -f %m is BSD syntax, so the credits cache is macOS-only. On Linux swap it for stat -c %Y.

claude-code-router support

If you run Claude Code through claude-code-router (ccr code), two fields are wrong by default: the model name shows the Anthropic tier Claude Code asked for, and the cost is priced against Anthropic's table for a model that actually ran on OpenRouter.

The script detects the local proxy via ANTHROPIC_BASE_URL and, when it matches:

  • reads Router.default from ~/.claude-code-router/config.json and shows ccr:<model> instead
  • replaces the cost with real OpenRouter spend: this session's delta plus remaining credits, from /api/v1/credits

Needs OPENROUTER_API_KEY in the environment. Credits are cached ~45s in ~/.cache/orc, curl is capped at 2s, and every failure path degrades back to the default display. Plain claude sessions are untouched because the whole block is gated on the base URL.

One caveat: it shows the configured default route, not per-request routing, so a mid-session /model switch or a background/think route won't be reflected.

Docs: https://code.claude.com/docs/en/statusline

#!/bin/bash
# ~/.claude/statusline.sh
# Two-line Claude Code statusline.
#
# Role in system:
# Invoked by Claude Code after each assistant message / on permission-mode or
# vim-mode changes (debounced 300ms). Receives JSON session data on stdin
# and prints two lines of formatted output. Coexists with the built-in
# right-side notification area (MCP errors, context-low warnings, etc.) —
# those are NOT rendered by this script, they share the row by design.
#
# Wired up from: ~/.claude/settings.json -> "statusLine.command"
# Related docs: https://code.claude.com/docs/en/statusline
#
# Layout:
# line 1: [Model] dir | branch
# line 2: <context-bar> N% | $cost | mm:ss | HH:MM:SS | 5h:N% 7d:N%
#
# Design notes:
# - No git status / dirty-check on purpose (user pref) — only `branch --show-current`.
# - jq is required. If absent the script silently degrades to "[unknown]".
# - Rate limits are absent for non-Pro/Max users; handled with `// empty`.
# - claude-code-router (ccr code) sessions are special-cased: the [Model] field
# shows CCR's real routed model, and the cost field is replaced with real
# OpenRouter spend (session delta + remaining credits) because Claude Code's
# own cost figure is wrong on the proxy. Both are gated on ANTHROPIC_BASE_URL
# pointing at the local CCR proxy, so plain `claude` sessions are unchanged.
# Needs $OPENROUTER_API_KEY in env; OpenRouter calls are cached in ~/.cache/orc.
input=$(cat)
# Bail to a useful default if jq is missing rather than printing nothing.
if ! command -v jq >/dev/null 2>&1; then
echo "[statusline: install jq]"
exit 0
fi
MODEL=$(echo "$input" | jq -r '.model.display_name // "unknown"')
# claude-code-router sessions: `ccr code` points ANTHROPIC_BASE_URL at the local
# proxy (:3456). In that case Claude Code only knows the Anthropic *tier* it
# requested (e.g. "Opus"), not the model CCR actually routes to underneath, so
# the statusline would lie. Detect the proxy and surface CCR's real default route
# instead. Gated on the base URL so plain `claude` sessions are unaffected.
# Note: shows Router.default — a mid-session `/model` switch or background/think
# routes may differ; this reflects the configured default, not per-request routing.
CCR_ACTIVE=""
case "$ANTHROPIC_BASE_URL" in
*127.0.0.1:3456*|*localhost:3456*)
CCR_ACTIVE=1
CCR_CONFIG="$HOME/.claude-code-router/config.json"
if [ -f "$CCR_CONFIG" ]; then
ROUTED=$(jq -r '.Router.default // empty' "$CCR_CONFIG" 2>/dev/null | sed 's/^[^,]*,//')
[ -n "$ROUTED" ] && MODEL="ccr:$ROUTED"
fi
;;
esac
DIR=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""')
PCT=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
COST=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
DURATION_MS=$(echo "$input" | jq -r '.cost.total_duration_ms // 0')
FIVE_H=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
WEEK=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
CYAN='\033[36m'
GREEN='\033[32m'
YELLOW='\033[33m'
RED='\033[31m'
DIM='\033[2m'
RESET='\033[0m'
# Color the context bar by pressure so high usage is visually loud.
if [ "$PCT" -ge 90 ]; then
BAR_COLOR="$RED"
elif [ "$PCT" -ge 70 ]; then
BAR_COLOR="$YELLOW"
else
BAR_COLOR="$GREEN"
fi
BAR_WIDTH=10
FILLED=$((PCT * BAR_WIDTH / 100))
[ "$FILLED" -gt "$BAR_WIDTH" ] && FILLED=$BAR_WIDTH
EMPTY=$((BAR_WIDTH - FILLED))
BAR=""
[ "$FILLED" -gt 0 ] && printf -v FILL "%${FILLED}s" && BAR="${FILL// /█}"
[ "$EMPTY" -gt 0 ] && printf -v PAD "%${EMPTY}s" && BAR="${BAR}${PAD// /░}"
MINS=$((DURATION_MS / 60000))
SECS=$(((DURATION_MS % 60000) / 1000))
DUR_FMT=$(printf '%dm %02ds' "$MINS" "$SECS")
COST_FMT=$(printf '$%.2f' "$COST")
# "Last responded" clock. The statusline fires immediately after each assistant
# message (and on mode changes), so the current wall-clock time at render time is
# a good proxy for when the agent last produced output. The displayed value stays
# frozen until the next render (the script never polls), so a session left idle
# for days keeps showing its last timestamp. The date is included UNCONDITIONALLY:
# at render time it is always "today", so any logic that drops the date when it
# equals today would drop it every time and never help. Carrying MM/DD means a
# frozen value left on screen for days reads e.g. "18/06 21:49" — self-evidently
# stale — instead of a bare "21:49:38" that looks deceptively recent.
NOW_FMT=$(date '+%d/%m %H:%M:%S')
# Cost segment. Default = Claude Code's own estimate. But on claude-code-router
# that figure is meaningless: Claude Code prices tokens against its Anthropic
# table for a model it thinks is Opus, while the request actually ran on a
# cheaper OpenRouter model. The real spend lives in the OpenRouter account, so
# for CCR sessions we replace it with "spent-this-session / remaining" pulled
# from OpenRouter's credits endpoint.
# - credits call is cached ~45s (statusline fires after every turn).
# - per-session usage is snapshotted on first render so we can show the delta.
# - curl is hard-capped at 2s and every failure degrades silently; the
# statusline must never hang or error on a flaky network.
COST_SEG="${YELLOW}${COST_FMT}${RESET}"
if [ -n "$CCR_ACTIVE" ] && [ -n "$OPENROUTER_API_KEY" ]; then
CACHE_DIR="$HOME/.cache/orc"
mkdir -p "$CACHE_DIR" 2>/dev/null
CRED="$CACHE_DIR/or-credits.json"
CRED_AGE=$(( $(date +%s) - $(stat -f %m "$CRED" 2>/dev/null || echo 0) ))
if [ ! -s "$CRED" ] || [ "$CRED_AGE" -ge 45 ]; then
curl -s --max-time 2 https://openrouter.ai/api/v1/credits \
-H "Authorization: Bearer $OPENROUTER_API_KEY" -o "$CRED.tmp" 2>/dev/null \
&& [ -s "$CRED.tmp" ] && mv "$CRED.tmp" "$CRED" || rm -f "$CRED.tmp" 2>/dev/null
fi
if [ -s "$CRED" ]; then
OR_USAGE=$(jq -r '.data.total_usage // empty' "$CRED" 2>/dev/null)
OR_CREDITS=$(jq -r '.data.total_credits // empty' "$CRED" 2>/dev/null)
if [ -n "$OR_USAGE" ] && [ -n "$OR_CREDITS" ]; then
SID=$(echo "$input" | jq -r '.session_id // "nosession"')
SNAP="$CACHE_DIR/session-$SID.usage"
[ -f "$SNAP" ] || printf '%s' "$OR_USAGE" > "$SNAP"
SNAP_USAGE=$(cat "$SNAP" 2>/dev/null || echo "$OR_USAGE")
OR_SPENT=$(awk -v a="$OR_USAGE" -v b="$SNAP_USAGE" 'BEGIN{d=a-b; if(d<0)d=0; printf "%.2f", d}')
OR_LEFT=$(awk -v c="$OR_CREDITS" -v u="$OR_USAGE" 'BEGIN{printf "%.2f", c-u}')
COST_SEG="${YELLOW}\$${OR_SPENT}${RESET} ${DIM}(\$${OR_LEFT} left)${RESET}"
fi
fi
fi
# Branch is cheap — no caching needed. Silent fail outside a repo.
BRANCH=""
if git rev-parse --git-dir >/dev/null 2>&1; then
B=$(git branch --show-current 2>/dev/null)
[ -n "$B" ] && BRANCH=" | 🌿 $B"
fi
# Rate-limits segment is omitted entirely for non-subscribers.
LIMITS=""
if [ -n "$FIVE_H" ]; then
LIMITS=$(printf '5h:%.0f%%' "$FIVE_H")
fi
if [ -n "$WEEK" ]; then
W=$(printf '7d:%.0f%%' "$WEEK")
LIMITS="${LIMITS:+$LIMITS }$W"
fi
# Collapse $HOME to ~ for readability without losing path context.
DIR_DISPLAY="$DIR"
case "$DIR" in
"$HOME") DIR_DISPLAY="~" ;;
"$HOME"/*) DIR_DISPLAY="~${DIR#$HOME}" ;;
esac
# Line 1: identity + location
printf '%b\n' "${CYAN}[$MODEL]${RESET} 📁 ${DIR_DISPLAY}${BRANCH}"
# Line 2: pressure + economics
LINE2="${BAR_COLOR}${BAR}${RESET} ${PCT}% | ${COST_SEG} | ${DIM}⏱ ${DUR_FMT}${RESET} | ${DIM}⏱ ${NOW_FMT}${RESET}"
[ -n "$LIMITS" ] && LINE2="${LINE2} | ${DIM}${LIMITS}${RESET}"
printf '%b\n' "$LINE2"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment