Statusline

workflow no always-loaded rules -- loads on demand updated 2026-08-04

Compact single-line Claude Code statusline: model + effort, multi-clone identity (.env.clone), dir + git branch, context-used % with a compaction warning, session cost, and 5h/7d rate-limit bars. Installs the script and wires the statusLine setting.

Tags

  • statusline
  • ui
  • context
  • cost
  • multi-clone

README

statusline

A compact, dependency-free Claude Code statusline rendered as a single line.

What It Does

Consumes the statusline JSON Claude Code pipes to the script on stdin and renders these sections, separated by |:

Section Source Notes
Model + effort .model.display_name, .effort.level 🧠 O-4.8 Max, 🐒 S-4.6, ⚠️ H-4.5. Family/version parsed generically β€” new model versions need no edits.
Clone identity .env.clone in cwd β›“ agent-2 when the working dir is a multi-clone checkout (reads AGENT_ID). Omitted otherwise.
Dir + branch .cwd, git Immediate directory name plus the current git branch.
Context used .context_window.total_input_tokens Γ· auto-compact budget ctx:42% (green/yellow/red by usage), measured against the compaction budget β€” min(context_window_size, 500000) β€” not the full window, so it reaches 100% as auto-compaction fires. Falls back to .context_window.remaining_percentage on older Claude Code.
Compaction warning derived ⚠ COMPACT SOON appears at β‰₯90% of the auto-compact budget, so you can /compact before an auto-compaction truncates the session. Override the budget via CCGM_CTX_COMPACT_BUDGET if you changed it with /autocompact.
Session cost .cost.total_cost_usd $1.23 for the current session. Omitted when Claude Code does not supply it.
Rate limits .rate_limits.five_hour, .rate_limits.seven_day 5h:30% β–ˆβ–‘β–‘β–‘β–‘ 2h14m and 7d:… bars with reset countdowns.

Every field is optional: a missing JSON key simply drops its section, so the bar degrades gracefully on older Claude Code versions.

Relationship to lib/statusline.sh and commands-utility

CCGM has long shipped a statusline script inside the commands-utility module (mirrored as lib/statusline.sh), but it was never wired to the statusLine setting β€” installing it left the user to configure settings.json by hand, and it did not surface clone identity, session cost, or an explicit compaction warning.

This module packages the statusline as a first-class, installable unit: it ships the script and a settings.partial.json that registers the statusLine setting, so start.sh --add statusline produces a working statusline with zero manual configuration. It is a superset of the commands-utility script (adds β›“ clone, $cost, and ⚠ COMPACT SOON).

Manual Installation

mkdir -p ~/.claude
cp statusline.sh ~/.claude/statusline.sh
chmod +x ~/.claude/statusline.sh

Then add to ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "bash $HOME/.claude/statusline.sh"
  }
}

Files

File Description
statusline.sh The statusline renderer (bash + jq, portable to bash 3.2 / BSD + GNU).
settings.partial.json Merged into settings.json to register the statusLine command.

Dependencies

jq only (already required by every CCGM tool). No external packages, no TUI library.

Will install

Path Action Target Type
statusline.sh β†’ statusline.sh script
settings.partial.json merge settings.json config

Dependencies

No dependencies.

Required by

No other module depends on this one.

Included in presets

Install this module

Agent prompt

Recommended for agent users -- hands the whole install off to your assistant.

Fetch https://cd23a9be.ccgm-site.pages.dev/modules/statusline.md and install this module into my Claude Code setup.

Native plugin marketplace

One command via the native plugin marketplace -- additive, does not merge settings.json.

claude plugin install statusline@ccgm

The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.

Manual, per file

Full control -- copy exactly the files you want from the sections below.

Files

Files

script (1)

statusline.sh

#!/usr/bin/env bash
# CCGM statusline β€” compact single-line bar for Claude Code.
#
# Consumes the statusline JSON on stdin and renders, separated by " | ":
#   model (+effort)  |  clone identity (if .env.clone)  |  dir + git branch
#   |  context used % (vs auto-compact budget)  |  ⚠ COMPACT SOON warning
#      when nearing that budget
#   |  session cost ($)  |  5h & 7d rate-limit bars
#
# Dependency-free beyond jq (already required by every CCGM tool). Portable to
# bash 3.2 / BSD + GNU. Every field is optional: a missing JSON key simply drops
# its section, so the bar degrades gracefully on older Claude Code versions.
#
# Wire it via the statusLine setting (this module ships settings.partial.json):
#   "statusLine": { "type": "command", "command": "bash ~/.claude/statusline.sh" }

input=$(cat)

# --- Model: render as "{family-letter}-{version}" with a tier-driven emoji.
# Family + version are parsed generically from display_name, so new VERSIONS of
# known families need no edits; a brand-new FAMILY needs one case branch.
model_raw=$(echo "$input" | jq -r '.model.display_name // ""')
model_ver=$(printf '%s' "$model_raw" | grep -oE '[0-9]+(\.[0-9]+)?' | head -n1)

case "$model_raw" in
  *Fable*)
    model_abbr="F${model_ver:+-$model_ver}"
    model_tier="flagship"
    ;;
  *Opus*)
    model_abbr="O${model_ver:+-$model_ver}"
    ver_major=${model_ver%%.*}; ver_minor=${model_ver#*.}
    [ "$ver_minor" = "$model_ver" ] && ver_minor=0
    if [ "${ver_major:-0}" -gt 4 ] || { [ "${ver_major:-0}" -eq 4 ] && [ "${ver_minor:-0}" -ge 6 ]; }; then
      model_tier="flagship"
    else
      model_tier="opus-other"
    fi
    ;;
  *Sonnet*) model_abbr="S${model_ver:+-$model_ver}"; model_tier="sonnet" ;;
  *Haiku*)  model_abbr="H${model_ver:+-$model_ver}"; model_tier="haiku" ;;
  "")       model_abbr=""; model_tier="" ;;
  *)        model_abbr=$(echo "$model_raw" | sed 's/Claude //;s/ .*//'); model_tier="unknown" ;;
esac

# --- Directory: immediate dir name only
cwd=$(echo "$input" | jq -r '.cwd // ""')
cwd_display="${cwd##*/}"

# --- Effort level (stdin > env > project local > project > user settings)
read_effort_from() {
  [ -f "$1" ] || return 1
  jq -r '.effortLevel // empty' "$1" 2>/dev/null
}
effort_raw=$(echo "$input" | jq -r '.effort.level // empty')
[ -z "$effort_raw" ] && effort_raw="${CLAUDE_CODE_EFFORT_LEVEL:-}"
if [ -z "$effort_raw" ] && [ -n "$cwd" ]; then
  effort_raw=$(read_effort_from "$cwd/.claude/settings.local.json")
  [ -z "$effort_raw" ] && effort_raw=$(read_effort_from "$cwd/.claude/settings.json")
fi
[ -z "$effort_raw" ] && effort_raw=$(read_effort_from "$HOME/.claude/settings.json")

case "$effort_raw" in
  low)    effort_abbr="L" ;;
  medium) effort_abbr="M" ;;
  high)   effort_abbr="H" ;;
  xhigh)  effort_abbr="XH" ;;
  max)    effort_abbr="Max" ;;
  *)      effort_abbr="" ;;
esac

# --- Multi-clone identity: read AGENT_ID from .env.clone if the cwd is a clone.
clone_id=""
if [ -n "$cwd" ] && [ -f "$cwd/.env.clone" ]; then
  clone_id=$(grep -E '^AGENT_ID=' "$cwd/.env.clone" 2>/dev/null | head -n1 | cut -d= -f2 | tr -d '"')
fi

# --- Git branch (skip optional locks to avoid hangs in multi-clone repos)
git_branch=""
if [ -d "$cwd/.git" ] || git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
  git_branch=$(git -C "$cwd" -c core.fsmonitor=false symbolic-ref --short HEAD 2>/dev/null \
    || git -C "$cwd" -c core.fsmonitor=false rev-parse --short HEAD 2>/dev/null)
fi

# --- Context. Prefer raw token counts so "used %" can be measured against the
# auto-compact budget rather than the full window. Claude Code's statusline JSON
# exposes total_input_tokens (input + cache_creation + cache_read, i.e. what is
# in context now) and context_window_size (200k, or 1M for extended-context
# models) β€” but NOT the auto-compact threshold itself. remaining_percentage is a
# full-window figure, kept only as a fallback for older Claude Code builds.
ctx_used_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // empty')
ctx_window_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty')
remaining=$(echo "$input" | jq -r '.context_window.remaining_percentage // empty')

# --- Session cost (USD). Claude Code exposes this under .cost.total_cost_usd.
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')

# --- Rate limits (5h session + 7-day)
five_hour=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
five_hour_resets=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
weekly=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
weekly_resets=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')

# --- ANSI color codes
RESET='\033[0m'
CYAN='\033[36m'
YELLOW='\033[33m'
GREEN='\033[32m'
RED='\033[31m'
DIM='\033[2m'
BLUE='\033[34m'
MAGENTA='\033[35m'
ORANGE='\033[38;5;208m'

# Compact usage bar: 5 chars wide
make_bar() {
  local pct=$1 color=$2
  local bar_len=5
  local filled=$(( (pct * bar_len + 50) / 100 ))
  [ "$filled" -gt "$bar_len" ] && filled=$bar_len
  [ "$filled" -lt 0 ] && filled=0
  local empty=$(( bar_len - filled ))
  local bar=""
  for ((i=0; i<filled; i++)); do bar="${bar}β–ˆ"; done
  local empty_part=""
  for ((i=0; i<empty; i++)); do empty_part="${empty_part}β–‘"; done
  printf "${color}%s${DIM}%s${RESET}" "$bar" "$empty_part"
}

SEP=$(printf " ${DIM}|${RESET} ")
sections=()

# Model with tier emoji + optional effort suffix
if [ -n "$model_abbr" ]; then
  effort_suffix=""
  if [ -n "$effort_abbr" ]; then
    case "$effort_abbr" in
      Max) effort_color="$RED" ;;
      XH)  effort_color="$ORANGE" ;;
      H)   effort_color="$YELLOW" ;;
      M)   effort_color="$GREEN" ;;
      *)   effort_color="$DIM" ;;
    esac
    effort_suffix="$(printf " ${effort_color}%s${RESET}" "$effort_abbr")"
  fi
  case "$model_tier" in
    flagship) sections+=("$(printf "${BLUE}🧠 %s${RESET}%s" "$model_abbr" "$effort_suffix")") ;;
    sonnet)   sections+=("$(printf "${ORANGE}🐒 %s${RESET}%s" "$model_abbr" "$effort_suffix")") ;;
    haiku)    sections+=("$(printf "${RED}⚠️ %s${RESET}%s" "$model_abbr" "$effort_suffix")") ;;
    *)        sections+=("$(printf "%s%s" "$model_abbr" "$effort_suffix")") ;;
  esac
fi

# Multi-clone identity (only present when .env.clone exists)
if [ -n "$clone_id" ]; then
  sections+=("$(printf "${MAGENTA}β›“ %s${RESET}" "$clone_id")")
fi

# Dir + git branch (combined)
dir_part="$(printf "${CYAN}%s${RESET}" "$cwd_display")"
if [ -n "$git_branch" ]; then
  dir_part="${dir_part} $(printf "${YELLOW}%s${RESET}" "$git_branch")"
fi
sections+=("$dir_part")

# Context used %, measured against the AUTO-COMPACT budget β€” the point where
# Claude Code auto-compacts β€” not the full window. For extended-context (1M)
# models that budget is capped far below capacity (500k "auto window" by
# default), so a full-window % reads ~half of true pressure and the compaction
# warning never fires before the surprise compaction. We model the budget as
# min(context_window_size, CAP): for a 200k model min() is the full window (it
# compacts near the top, so nothing changes); for a 1M model it becomes 500k, so
# the bar hits 100% exactly when compaction fires. CAP is overridable for anyone
# who changes it via /autocompact, since the JSON does not expose the threshold.
COMPACT_BUDGET_CAP=${CCGM_CTX_COMPACT_BUDGET:-500000}
used_int=""
if [ -n "$ctx_used_tokens" ] && [ -n "$ctx_window_size" ] && [ "$ctx_window_size" -gt 0 ] 2>/dev/null; then
  used_tokens=$(printf '%.0f' "$ctx_used_tokens")
  budget=$ctx_window_size
  [ "$budget" -gt "$COMPACT_BUDGET_CAP" ] && budget=$COMPACT_BUDGET_CAP
  used_int=$(( used_tokens * 100 / budget ))
  [ "$used_int" -gt 100 ] && used_int=100
elif [ -n "$remaining" ]; then
  # Fallback: older Claude Code without raw token fields β€” full-window %.
  remaining_int=$(printf '%.0f' "$remaining")
  used_int=$((100 - remaining_int))
fi

if [ -n "$used_int" ]; then
  if [ "$used_int" -lt 60 ]; then
    ctx_color="$GREEN"
  elif [ "$used_int" -lt 85 ]; then
    ctx_color="$YELLOW"
  else
    ctx_color="$RED"
  fi
  sections+=("$(printf "${ctx_color}ctx:${used_int}%%${RESET}")")
  # Compaction warning: flag loudly as we near the auto-compact budget so the
  # user can /compact or wrap up deliberately before an auto-compaction fires.
  if [ "$used_int" -ge 90 ]; then
    sections+=("$(printf "${RED}⚠ COMPACT SOON${RESET}")")
  fi
fi

# Session cost in USD
if [ -n "$cost_usd" ]; then
  cost_fmt=$(printf '%.2f' "$cost_usd" 2>/dev/null)
  if [ -n "$cost_fmt" ]; then
    sections+=("$(printf "${DIM}\$%s${RESET}" "$cost_fmt")")
  fi
fi

# 5-hour rate limit with bar and reset countdown
if [ -n "$five_hour" ]; then
  five_int=$(printf '%.0f' "$five_hour")
  if [ "$five_int" -lt 60 ]; then rl_color="$GREEN"
  elif [ "$five_int" -lt 85 ]; then rl_color="$YELLOW"
  else rl_color="$RED"; fi
  five_part="$(printf "${rl_color}5h:${five_int}%%${RESET} ")$(make_bar "$five_int" "$rl_color")"
  if [ -n "$five_hour_resets" ]; then
    now=$(date +%s)
    diff=$(( five_hour_resets - now ))
    if [ "$diff" -gt 0 ]; then
      hours=$(( diff / 3600 )); mins=$(( (diff % 3600) / 60 ))
      if [ "$hours" -gt 0 ]; then
        five_part="${five_part} $(printf "${DIM}${hours}h${mins}m${RESET}")"
      else
        five_part="${five_part} $(printf "${DIM}${mins}m${RESET}")"
      fi
    fi
  fi
  sections+=("$five_part")
fi

# 7-day rate limit with bar and reset countdown
if [ -n "$weekly" ]; then
  weekly_int=$(printf '%.0f' "$weekly")
  if [ "$weekly_int" -lt 60 ]; then wk_color="$GREEN"
  elif [ "$weekly_int" -lt 85 ]; then wk_color="$YELLOW"
  else wk_color="$RED"; fi
  wk_part="$(printf "${wk_color}7d:${weekly_int}%%${RESET} ")$(make_bar "$weekly_int" "$wk_color")"
  if [ -n "$weekly_resets" ]; then
    now=$(date +%s)
    diff=$(( weekly_resets - now ))
    if [ "$diff" -gt 0 ]; then
      days=$(( diff / 86400 )); hours=$(( (diff % 86400) / 3600 ))
      if [ "$days" -gt 0 ]; then
        wk_part="${wk_part} $(printf "${DIM}${days}d${hours}h${RESET}")"
      else
        mins=$(( (diff % 3600) / 60 ))
        wk_part="${wk_part} $(printf "${DIM}${hours}h${mins}m${RESET}")"
      fi
    fi
  fi
  sections+=("$wk_part")
fi

# Join sections with pipe separator
output=""
for i in "${!sections[@]}"; do
  if [ "$i" -gt 0 ]; then output="${output}${SEP}"; fi
  output="${output}${sections[$i]}"
done

printf "%s" "$output"
config (1)

settings.partial.json

Merged into ~/.claude/settings.json -- a fragment, not a replacement.

{
  "statusLine": {
    "type": "command",
    "command": "bash $HOME/.claude/statusline.sh"
  }
}