statuslin.es

Two-line: dir/git/model/advisor/effort on top + Caveman, RTK, context %, 5h/7d rate limits, and session cost below

Updated 2026-08-18

2 copies

Preview

Clean repo
app · main · Opus 4.8 · high ctx 22% 44k · 5h 26% ↻2h6m · 7d 7% ↻2d0h · cost $0.41 · msg $0.757
New session
app · main · Opus 4.8 · high ctx 0% · cost $0.00
Dirty branch
app · feat/auth* · Sonnet 4.6 · med ctx 48% 96k · 5h 40% ↻1h11m · 7d 18% ↻2d21h · cost $0.41 · msg $0.307
Near-full
app · main · Opus 4.8 · max · Explanatory ctx 91% 182k · 5h 88% ↻17m · 7d 61% ↻19h59m · cost $4.12 · msg $2.827
1M context
app · main · Fable 5 · xhigh ctx 64% 640k · 5h 33% ↻3h29m · 7d 80% ↻4d23h · cost $0.41 · msg $1.939
Post-compact
app · main · Haiku 4.5 ctx 0% · 5h 52% ↻4h1m · cost $0.41
Worktree
feature · worktree-feature · Opus 4.8 · low ctx 37% 74k · 5h 44% ↻2h49m · 7d 20% ↻1d6h · cost $0.41 · msg $1.207
Non-git
scratch · Opus 4.8 · high ctx 22% 44k · cost $0.41 · msg $0.757

Source

#!/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

Dreambase Panel

150 copies

╭─ CONTEXT ────────────────────────────────────────────╮ ████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD ├─ STATS ──────────────────────────────────────────────┤ 44.0K 1.4K $0.41 ⏱ 10m 12s +128/-34 ├─ REPO ───────────────────────────────────────────────┤ Opus 4.8 · main · 16:21 ╰──────────────────────────────────────────────────────╯
app · main · Opus-4.8 · effort high · ctx 22% · 5h 26% ↻2h6m · 7d 7% ↻2d0h
Opus 4.8 [high] ~/app main 16:20:55 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m
Opus 4.8 main app 44k/200k $0.41

Activity Feed

36 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
████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD 44.0K 1.4K $0.41 ⏱ 10m 12s +128/-34 Opus 4.8 · main · 16:22