Two-line: dir/git/model/advisor/effort on top + Caveman, RTK, context %, 5h/7d rate limits, and session cost below
4 copies
Using the Claude desktop app? Add your status line with statusline-anywhere.
Preview
Clean repo
app · main · Opus 4.8 · high
ctx 22% 44k · 5h 26% ↻2h6m · 7d 7% ↻2d0h · cost $0.41 · msg $0.757New session
app · main · Opus 4.8 · high
ctx 0% · cost $0.00Dirty branch
app · feat/auth* · Sonnet 4.6 · med
ctx 48% 96k · 5h 40% ↻1h11m · 7d 18% ↻2d21h · cost $0.41 · msg $0.307Near-full
app · main · Opus 4.8 · max · Explanatory
ctx 91% 182k · 5h 88% ↻17m · 7d 61% ↻19h59m · cost $4.12 · msg $2.8271M context
app · main · Fable 5 · xhigh
ctx 64% 640k · 5h 33% ↻3h29m · 7d 80% ↻4d23h · cost $0.41 · msg $1.939Post-compact
app · main · Haiku 4.5
ctx 0% · 5h 52% ↻4h1m · cost $0.41Worktree
feature · worktree-feature · Opus 4.8 · low
ctx 37% 74k · 5h 44% ↻2h49m · 7d 20% ↻1d6h · cost $0.41 · msg $1.207Non-git
scratch · Opus 4.8 · high
ctx 22% 44k · cost $0.41 · msg $0.757Source
#!/bin/bash
input=$(cat)
dir=$(echo "$input" | jq -r '.workspace.current_dir')
model=$(echo "$input" | jq -r '.model.display_name')
model_id=$(echo "$input" | jq -r '.model.id')
output_style=$(echo "$input" | jq -r '.output_style.name')
transcript=$(echo "$input" | jq -r '.transcript_path // empty')
dim="\033[2m"
reset="\033[0m"
bold="\033[1m"
cyan="\033[36m"
yellow="\033[33m"
green="\033[32m"
red="\033[31m"
magenta="\033[35m"
blue="\033[34m"
# Color helper: green <50, yellow 50-79, red >=80
color_for_pct() {
local p=$1
if [ "$p" -ge 80 ]; then echo "$red"
elif [ "$p" -ge 50 ]; then echo "$yellow"
else echo "$green"; fi
}
# Format a token count → "1.2M" / "347k" / "812"
fmt_tokens() {
local t=$1
if [ "$t" -ge 1000000 ]; then
printf "%d.%dM" $((t / 1000000)) $(((t % 1000000) / 100000))
elif [ "$t" -ge 1000 ]; then
printf "%dk" $(((t + 500) / 1000))
else
printf "%d" "$t"
fi
}
# Format a raw model id → the display name Claude itself uses.
# "claude-opus-5" → "Opus 5", "claude-haiku-4-5-20251001" → "Haiku 4.5",
# "claude-opus-5[1m]" → "Opus 5", bare alias "opus" → "Opus".
fmt_model_id() {
local m=$1
m=${m%%\[*} # drop [1m] context-variant suffix
m=${m#claude-} # drop vendor prefix
m=$(printf '%s' "$m" | sed -E 's/-20[0-9]{6}$//') # drop trailing date stamp
local family=${m%%-*}
local ver=""
[ "$m" != "$family" ] && ver=$(printf '%s' "${m#*-}" | tr '-' '.')
family="$(printf '%s' "${family:0:1}" | tr '[:lower:]' '[:upper:]')${family:1}"
if [ -n "$ver" ]; then printf '%s %s' "$family" "$ver"; else printf '%s' "$family"; fi
}
# True when two model identifiers name the same model. Handles the case where one
# side is a bare alias with no version ("sonnet", as written in settings.json) and
# the other is a fully resolved id ("claude-sonnet-5", as stamped in transcripts):
# comparing the rendered labels alone would call those two different models.
same_model() {
local a b fa fb
a=$(fmt_model_id "$1")
b=$(fmt_model_id "$2")
[ "$a" = "$b" ] && return 0
fa=${a%% *} # family alone; whole string when unversioned
fb=${b%% *}
# Unversioned on either side → that side is an alias, which can only mean the
# current model of its family, so family equality is all we can require.
if [ "$a" = "$fa" ] || [ "$b" = "$fb" ]; then
[ "$fa" = "$fb" ] && return 0
fi
return 1
}
# Format a duration in seconds → "2h13m" / "47m" / "3d4h"
fmt_duration() {
local s=$1
[ "$s" -lt 0 ] && s=0
local d=$((s / 86400))
local h=$(((s % 86400) / 3600))
local m=$(((s % 3600) / 60))
if [ "$d" -gt 0 ]; then
printf "%dd%dh" "$d" "$h"
elif [ "$h" -gt 0 ]; then
printf "%dh%dm" "$h" "$m"
else
printf "%dm" "$m"
fi
}
# --- Line 1: orientation ---
dir_name=$(basename "$dir")
git_info=""
if git -C "$dir" rev-parse --git-dir &>/dev/null; then
branch=$(git -C "$dir" -c core.useBuiltinFSMonitor=false rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')
if [ -n "$branch" ]; then
if git -C "$dir" -c core.useBuiltinFSMonitor=false diff-index --quiet HEAD -- 2>/dev/null; then
git_info="$branch"
else
git_info="${branch}*"
fi
fi
fi
# Claude's own display name, verbatim apart from the redundant vendor prefix:
# "Claude Opus 5 (1M context)" → "Opus 5 (1M context)". Nothing is derived or
# abbreviated here, so the label always matches what /model shows.
short_model=$(echo "$model" | sed -E 's/^Claude //')
if [[ "$model_id" == *"opus"* ]]; then
model_str="${bold}${yellow}${short_model}${reset}"
elif [[ "$model_id" == *"sonnet"* ]]; then
model_str="${cyan}${short_model}${reset}"
elif [[ "$model_id" == *"haiku"* ]]; then
model_str="${blue}${short_model}${reset}"
else
model_str="${short_model}"
fi
# --- Advisor model, appended to the model segment as "Sonnet 5 + Opus 5". ---
# Claude Code stamps every assistant transcript entry with the advisor model it
# actually resolved for that request, so the transcript — not settings.json — is
# the authoritative source: it already accounts for /advisor session-only
# overrides, the --advisor flag, AND the capability gate (advisor must be at
# least as capable as the main model; when it isn't, the field is simply absent
# and no badge should be shown).
# Settings are consulted ONLY when the transcript has no assistant entry yet
# (fresh session, first render) — otherwise an absent stamp would be misread as
# "unknown" and the badge would claim an advisor that isn't running.
advisor_model=""
if [ -n "$transcript" ] && [ -r "$transcript" ]; then
# 1 MB tail keeps this O(1) regardless of transcript size; the last complete
# assistant line reflects the current state.
adv_line=$(tail -c 1000000 "$transcript" 2>/dev/null | grep '"type":"assistant"' | tail -1)
if [ -n "$adv_line" ]; then
advisor_model=$(printf '%s' "$adv_line" | jq -r '.advisorModel // empty' 2>/dev/null)
else
advisor_model=$(jq -r '.advisorModel // empty' "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" 2>/dev/null)
fi
fi
if [ -n "$advisor_model" ]; then
# Whitelist-strip before rendering: never let a transcript field reach the
# terminal with escape bytes intact.
advisor_model=$(printf '%s' "$advisor_model" | tr -cd 'a-zA-Z0-9._][-')
if [ -n "$advisor_model" ]; then
# Only render when the advisor differs from the main model — "Opus 5 +
# Opus 5" carries no information. same_model() absorbs both the
# 1M-context suffix and the bare-alias form settings.json uses.
same_model "$advisor_model" "$model_id" \
|| model_str+="${dim} + $(fmt_model_id "$advisor_model")${reset}"
fi
fi
ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' | cut -d. -f1)
ctx_color=$(color_for_pct "$ctx_pct")
now=$(date +%s)
fh_pct_raw=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
sd_pct_raw=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
effort=$(echo "$input" | jq -r '.effort.level // empty')
# "ultracode" is not an effort level — it is a session flag meaning "xhigh effort
# plus standing dynamic-workflow orchestration", and Claude Code collapses it to
# plain "xhigh" before the payload is built (settings schema: "Session-scoped —
# interactive toggles never persist it"), so neither this payload nor any
# settings file can tell the two apart.
# The one place it does survive is the transcript: /effort records its result as
# a user entry whose whole content is "<local-command-stdout>Set effort level to
# <x> ...</local-command-stdout>". Take the last such entry and, only when the
# payload already says xhigh, upgrade the label.
# Matched with jq on the message content rather than a plain grep on purpose: any
# session that merely *discusses* effort levels ends up with that sentence sitting
# inside some tool output, and a raw grep happily matches it. Requiring the
# content to START with the tag keeps only genuine command results.
# Costs a full-transcript jq pass (~23 ms on a 2 MB transcript, measured — and
# faster than the equivalent grep, which has to scan for a longer literal). Only
# runs when the level is xhigh, since that is the sole ambiguous case.
# A later "/effort xhigh" appends a newer marker and self-corrects. Known hole:
# on a resumed session an old ultracode marker outlives the state that set it,
# so a persisted-xhigh default with no newer /effort would read as ultracode.
if [ "$effort" = "xhigh" ] && [ -n "$transcript" ] && [ -r "$transcript" ]; then
eff_marker=$(jq -r 'select(.type=="user") | .message.content | strings
| select(startswith("<local-command-stdout>Set effort level to "))' \
"$transcript" 2>/dev/null | tail -1)
eff_marker=${eff_marker#*Set effort level to }
[ "${eff_marker%% *}" = "ultracode" ] && effort="ultracode"
fi
case "$effort" in
ultracode) effort_str="${bold}${magenta}ultracode${reset}" ;;
max) effort_str="${bold}${red}max${reset}" ;;
xhigh) effort_str="${bold}${red}xhigh${reset}" ;;
high) effort_str="${red}high${reset}" ;;
medium) effort_str="${yellow}med${reset}" ;;
low) effort_str="${green}low${reset}" ;;
"") effort_str="" ;;
*) effort_str="${dim}${effort}${reset}" ;;
esac
sep="${dim} · ${reset}"
# --- Line 1: orientation (dir · git · model · effort · output_style) ---
line1="${bold}${dir_name}${reset}"
[ -n "$git_info" ] && line1+="${sep}${magenta}${git_info}${reset}"
line1+="${sep}${model_str}"
[ -n "$effort_str" ] && line1+="${sep}${effort_str}"
if [ "$output_style" != "null" ] && [ "$output_style" != "default" ]; then
line1+="${sep}${dim}${output_style}${reset}"
fi
# --- Caveman badge: reads the flag file the caveman plugin's SessionStart
# hook writes. Same hardening as the plugin's own caveman-statusline.sh —
# reject symlinks, hard-cap read, strip to a whitelist charset — so a
# malicious flag file can't inject ANSI/OSC escapes into the statusline.
cvm_label=""
caveman_flag="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/.caveman-active"
if [ -f "$caveman_flag" ] && [ ! -L "$caveman_flag" ]; then
cmode=$(head -c 64 "$caveman_flag" 2>/dev/null | tr -d '\n\r' | tr '[:upper:]' '[:lower:]')
cmode=$(printf '%s' "$cmode" | tr -cd 'a-z0-9-')
case "$cmode" in
off|lite|full|ultra|wenyan-lite|wenyan|wenyan-full|wenyan-ultra|commit|review|compress)
if [ -z "$cmode" ] || [ "$cmode" = "full" ]; then
cvm_label="CVM"
else
csuffix=$(printf '%s' "$cmode" | tr '[:lower:]' '[:upper:]')
cvm_label="CVM:${csuffix}"
fi
;;
esac
fi
# Current-session tokens-saved estimate, straight from the plugin's own
# per-session aggregator (parses $transcript, the active session's own
# transcript — no --all, so this is scoped to this session, not lifetime).
# find() is scoped to the plugin's own cache dir and capped at depth 6, so
# the walk is a handful of stat() calls, not a tree scan.
if [ -n "$cvm_label" ] && [ -n "$transcript" ] && [ "${CAVEMAN_STATUSLINE_SAVINGS:-1}" != "0" ]; then
cvm_stats_js=$(find "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/cache/caveman" -maxdepth 6 -iname 'caveman-stats.js' 2>/dev/null | head -1)
if [ -n "$cvm_stats_js" ]; then
# Node's toLocaleString() groups thousands with U+00A0 (non-breaking
# space), not ASCII space, and the line also carries a trailing
# "(~65% of output)" — so strip everything from the first '(' onward
# first, THEN drop all non-digit bytes (handles any separator byte).
cvm_raw=$(node "$cvm_stats_js" --session-file "$transcript" 2>/dev/null | grep 'Est. tokens saved:')
cvm_saved=$(printf '%s' "${cvm_raw%%(*}" | tr -cd '0-9')
if [ -n "$cvm_saved" ] && [ "$cvm_saved" -gt 0 ]; then
cvm_saved_fmt=$(fmt_tokens "$cvm_saved")
cvm_label+=" ${bold}${cvm_saved_fmt}"
fi
fi
fi
caveman_badge=""
[ -n "$cvm_label" ] && caveman_badge="\033[38;5;172m${cvm_label}\033[0m"
[ -n "$caveman_badge" ] && line1+="${sep}${caveman_badge}"
# --- RTK badge: token savings scoped to the current project (cwd). ---
rtk_badge=""
if command -v rtk &>/dev/null; then
rtk_saved=$(cd "$dir" 2>/dev/null && rtk gain --project --format json 2>/dev/null | jq -r '.summary.total_saved // empty' 2>/dev/null)
case "$rtk_saved" in
''|*[!0-9]*) rtk_saved="" ;;
esac
if [ -n "$rtk_saved" ] && [ "$rtk_saved" -gt 0 ]; then
rtk_saved_fmt=$(fmt_tokens "$rtk_saved")
rtk_badge="\033[38;5;208mRTK ${bold}${rtk_saved_fmt}\033[0m"
fi
fi
[ -n "$rtk_badge" ] && line1+="${sep}${rtk_badge}"
# --- Line 2: usage (ctx + tokens · 5h · 7d) ---
ctx_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // empty')
ctx_tok_str=""
[ -n "$ctx_tokens" ] && [ "$ctx_tokens" -gt 0 ] && ctx_tok_str=" ${dim}$(fmt_tokens "$ctx_tokens")${reset}"
line2="${dim}ctx${reset} ${ctx_color}${ctx_pct}%${reset}${ctx_tok_str}"
# awk, not bash's builtin printf — same locale trap as the cost block below:
# under LC_NUMERIC=sk_SK.UTF-8 the builtin rejects jq's dot-decimal "31.4" as an
# "invalid number" and the percentage silently collapses to empty.
if [ -n "$fh_pct_raw" ]; then
fh_pct=$(LC_ALL=C awk -v n="$fh_pct_raw" 'BEGIN{printf "%.0f", n}')
fh_color=$(color_for_pct "$fh_pct")
fh_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
fh_suffix=""
[ -n "$fh_reset" ] && fh_suffix=" ${dim}↻$(fmt_duration $((fh_reset - now)))${reset}"
line2+="${sep}${dim}5h${reset} ${fh_color}${fh_pct}%${reset}${fh_suffix}"
fi
if [ -n "$sd_pct_raw" ]; then
sd_pct=$(LC_ALL=C awk -v n="$sd_pct_raw" 'BEGIN{printf "%.0f", n}')
sd_color=$(color_for_pct "$sd_pct")
sd_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
sd_suffix=""
[ -n "$sd_reset" ] && sd_suffix=" ${dim}↻$(fmt_duration $((sd_reset - now)))${reset}"
line2+="${sep}${dim}7d${reset} ${sd_color}${sd_pct}%${reset}${sd_suffix}"
fi
# Session total: Claude Code ships the authoritative figure in the payload
# (cost.total_cost_usd), so no transcript arithmetic is needed. This also counts
# subagent and workflow spend, which a per-message token sum over the main
# transcript silently misses.
sess_cost=$(echo "$input" | jq -r '.cost.total_cost_usd // empty' 2>/dev/null)
# Per-message cost still has to be estimated — the payload reports the current
# context's token usage but no price for it. Rates are USD per million tokens,
# standard <200k tier; w5/w1 = 5-minute / 1-hour prompt-cache write, r = cache read.
rates_def='def rates(m): if (m|test("opus")) then {i:15,o:75,w5:18.75,w1:30,r:1.5} elif (m|test("haiku")) then {i:1,o:5,w5:1.25,w1:2,r:0.1} elif (m|test("sonnet")) then {i:3,o:15,w5:3.75,w1:6,r:0.3} else {i:3,o:15,w5:3.75,w1:6,r:0.3} end;'
# Current context: cost of the most recent API call (context_window.current_usage,
# no 5m/1h split → treat cache creation as 5m write). No transcript read needed.
cur_cost=$(echo "$input" | jq -r "$rates_def"'
rates(.model.id // "") as $R
| (.context_window.current_usage) as $u
| if $u == null then empty else
( (($u.input_tokens//0)*$R.i + ($u.output_tokens//0)*$R.o + ($u.cache_creation_input_tokens//0)*$R.w5 + ($u.cache_read_input_tokens//0)*$R.r) / 1000000 )
end' 2>/dev/null)
# awk, not bash's builtin printf: under a non-C LC_NUMERIC (e.g. sk_SK.UTF-8,
# comma decimal), bash's printf rejects jq's dot-decimal numbers outright
# ("invalid number" -> silently prints $0.00) because it validates the string
# against the locale's decimal separator before the LC_ALL=C override can take
# effect for a builtin. awk's own strtod-based number parsing isn't picky
# about the separator, so it converts correctly regardless of locale; LC_ALL=C
# only pins the OUTPUT formatting to a dot, for a consistent "$3.41" look.
if [ -n "$sess_cost" ]; then
line2+="${sep}${dim}cost${reset} \$$(LC_ALL=C awk -v n="$sess_cost" 'BEGIN{printf "%.2f", n}')"
fi
if [ -n "$cur_cost" ]; then
line2+="${sep}${dim}msg \$$(LC_ALL=C awk -v n="$cur_cost" 'BEGIN{printf "%.3f", n}')${reset}"
fi
printf "%b\n%b" "$line1" "$line2"
What it shows
- Current directory name on the first line
- Git branch, with a * when the working tree is dirty
- Model display name, colored for Opus, Sonnet, and Haiku
- Effort level (low, med, high, xhigh, max), omitted when the payload has no effort field
- Non-default output style, such as Explanatory
- Advisor model beside the main model when it differs (from the transcript, or from settings.json on a fresh session)
- Optional Caveman badge (CVM or CVM:MODE) and session tokens saved
- Optional RTK project token-savings badge, if rtk is on PATH
- Context window usage as a percentage and abbreviated token count
- 5-hour and 7-day quota percentages, with time until reset
- Session cost from Claude Code's payload
- Estimated cost of the latest API call, labeled msg
Requirements
- Bash
- jq, to parse the status line JSON on stdin
- git, for the branch segment (omitted in a non-git directory)
- awk, so percentages and dollar amounts format under non-C locales
- Optional: the Caveman plugin and node, for the CVM savings figure
- Optional: the rtk CLI, for the RTK badge
Behavior notes
- Prints two lines in every previewed session.
- A dirty feature branch shows feat/auth*. A scratch directory drops the git segment.
- Worktree sessions use the worktree folder name and its git branch, not the original project dir.
- Haiku post-compact has no effort field, so that segment is gone. medium renders as med. max and xhigh are bold red.
- Explanatory appears only when output_style is not default.
- Fresh sessions show ctx 0% and cost $0.00, and hide the 5h/7d windows and the msg estimate.
- After compact, context is 0% and the 7-day window is absent, matching a payload that only has the 5-hour limit.
- Percentages turn yellow from 50 and red from 80. Near-full hits red on ctx 91% and 5h 88%. The 1M-context preview hits red on 7d 80%.
- Session cost follows cost.total_cost_usd ($4.12 when near-full, $0.41 otherwise).
- The msg figure is an estimate from hardcoded rates. Families other than opus, haiku, and sonnet (Fable in the 1M preview) are priced like Sonnet.
- Caveman, RTK, and advisor do not appear in these previews. The sandbox has no plugin, no rtk binary, and no advisorModel stamp on the transcript.
More status lines
Pace Dot Meters
92 copies
Opus 4.8 [high] ~/app main
16:20:55 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m
Activity Feed
51 copies
Opus 4.8 high ✦ │ ctx 22% │ app (main) │ PR #1287 ● │ $0.41 · 10m +128 -34
Session ● ● ● ● ● ● ● ● ○ ○ ○ 74% left Resets in 2h 6m
Weekly ● ● ● ● ● ● ● ● ● ● ○ 93% left Resets in 2d 0h
Fable ● ● ● ● ● ● ● ● ● ○ ○ 85% left Resets in 2d 0h
◐ Read:index.ts ✓ Read×1 ✓ Bash×1 ✓ Edit×1 ✓ Grep×1
◐ code-reviewer Review the change for regressions
One Dark Triptych
13 copies
Model: Opus 4.8 | Thinking: high | Context: [▰▰▱▱▱▱▱▱▱▱] 45k/200k (22%) | cwd: ~/app | ⎇ main | (+0,-0)
Cost: $0.41 | Block: 2hr 6m | Weekly: 7.0% | Session: 10m | In: 916.7 t/s | Out: 29.2 t/s | Cached: 2k | Session: [▰▱▱▱▱] 26.0%
Mem: 0.8G/1.0G | (*゚∀゚*) | Session ID: b8e1c0d2-4a6f-4e2a-9c1b-3f5d7a9e2c10
Pace Arrows
5 copies
Opus 4.8 (200K) high | app (main)
██░░░░░░░░ 22% 200K | 5h 26% ⇣32% 2h 7d 7% ⇣63% 2d
Burn Rate Bars
4 copies
📁 app 🌿 main 🧠 Opus
🧊 22% [==--------] 💰 $0.41 ($2.41/h)
⏳ 5h: 26% [==--------] 7d: 7% [----------]
Gradient Dashboard
3 copies
◆ Opus 4.8 │ ██░░░░░░░░ 22% 200k │ $0.41 │ 10m12s │ 5h:26% 7d:7%
⎇main │ +128/-34 │ app