Two-line status with a block context bar and quarter-step dot meters (◔◑◕●) for the 5-hour and weekly limits (fill shows usage, color shows whether you are ahead of or behind pace), plus model, effort, git and cost. Needs no jq.
MIT licensed · original source
Updated 2026-06-29
63 copies
Preview
Clean repo
Opus 4.8 [high] ~/app main
16:20:55 | ⛁ ██░░░░░░░░ 22% | 5h ●◔○○○ 26% ↻2h7m | 7d ◔○○○○○○ 7% ↻2d1h │ $0.41 ⏱ 10m
New session
Opus 4.8 [high] ~/app main
16:20:56 │ ⏱ 10m
Dirty branch
Sonnet 4.6 [medium] ~/app feat/auth (+2)
16:20:57 | ⛁ █████░░░░░ 48% | 5h ●●○○○ 40% ↻1h12m | 7d ●◔○○○○○ 18% ↻2d22h │ $0.41 ⏱ 10m
Near-full
Opus 4.8 [max] ~/app main
16:20:59 | ⛁ █████████░ 91% | 5h ●●●●◔ 88% ↻18m | 7d ●●●●◔○○ 61% ↻20h0m │ $4.12 ⏱ 10m
1M context
Fable 5 [xhigh] ~/app main
16:21:00 | ⛁ ██████░░░░ 64% | 5h ●◑○○○ 33% ↻3h30m | 7d ●●●●●◑○ 80% ↻5d │ $0.41 ⏱ 10m
Post-compact
Haiku 4.5 ~/app main
16:21:01 | 5h ●●◑○○ 52% ↻4h2m │ $0.41 ⏱ 10m
Worktree
Opus 4.8 [low] ~/.wt/feature worktree-feature
16:21:02 | ⛁ ████░░░░░░ 37% | 5h ●●○○○ 44% ↻2h50m | 7d ●◔○○○○○ 20% ↻1d7h │ $0.41 ⏱ 10m
Non-git
Opus 4.8 [high] ~/scratch
16:21:03 | ⛁ ██░░░░░░░░ 22% │ $0.41 ⏱ 10m
Source
#!/usr/bin/env bash
# cc-statusline — two-line Claude Code status line, pure POSIX shell + awk.
# Reads the status JSON on stdin; needs no node/bun/jq. git & kubectl are
# optional and only invoked when present.
#
# Line 1: model [effort] cwd ⎈ k8s(ns) git-branch (+changes) HH:MM:SS
# Line 2: context bar | 5h <dots> <used%> ↻rem | 7d <dots> <used%> ↻rem │ $cost ⏱ dur
# dots FILL = usage % consumed (5h: 5 dots, 7d: 7 dots; in-progress dot = quarter steps)
# dots COLOR = burn pace (used% vs time-elapsed%): blue ok, yellow tight, red too fast
# ↻rem = time left until the window resets
#
# Customize via env: CC_SL_FULL CC_SL_HALF CC_SL_EMPTY (dot glyphs),
# CC_SL_PACE_FLOOR (used% below which pace color stays blue; default 5).
input=$(cat)
# --- parse the JSON once (path-aware scalar extraction) -----------------------
read_fields() {
printf '%s' "$input" | awk '
{ rec = rec $0 "\n" }
function ws() { while (pos <= len) { c = substr(S, pos, 1)
if (c == " " || c == "\t" || c == "\n" || c == "\r") pos++; else break } }
function str( out, c, nc) { pos++; out = ""
while (pos <= len) { c = substr(S, pos, 1)
if (c == "\\") { nc = substr(S, pos + 1, 1)
if (nc == "n") out = out "\n"; else if (nc == "t") out = out "\t"
else out = out nc; pos += 2; continue }
if (c == "\"") { pos++; break }
out = out c; pos++ }
return out }
function prim( out, c) { out = ""
while (pos <= len) { c = substr(S, pos, 1)
if (c == "," || c == "}" || c == "]" || c == " " || c == "\t" || c == "\n" || c == "\r") break
out = out c; pos++ }
return out }
function value(path, c) { ws(); c = substr(S, pos, 1)
if (c == "{") obj(path)
else if (c == "[") arr(path)
else if (c == "\"") V[path] = str()
else V[path] = prim() }
function obj(path, k) { pos++; ws()
if (substr(S, pos, 1) == "}") { pos++; return }
while (1) { ws(); k = str(); ws(); pos++ # skip :
value(path "." k); ws(); c = substr(S, pos, 1); pos++
if (c == ",") continue; else break } } # skip , or }
function arr(path, i) { pos++; ws()
if (substr(S, pos, 1) == "]") { pos++; return }
i = 0
while (1) { value(path "." i); i++; ws(); c = substr(S, pos, 1); pos++
if (c == ",") continue; else break } }
END {
S = rec; len = length(S); pos = 1; value("")
eff = (V[".effort.level"] != "") ? V[".effort.level"] : V[".effort"]
print V[".model.display_name"]
print eff
print V[".workspace.current_dir"]
print V[".context_window.used_percentage"]
print V[".rate_limits.five_hour.used_percentage"]
print V[".rate_limits.five_hour.resets_at"]
print V[".rate_limits.seven_day.used_percentage"]
print V[".rate_limits.seven_day.resets_at"]
print V[".cost.total_cost_usd"]
print V[".cost.total_duration_ms"]
}'
}
{
IFS= read -r model
IFS= read -r effort
IFS= read -r cwd
IFS= read -r ctx
IFS= read -r fh_used
IFS= read -r fh_reset
IFS= read -r sd_used
IFS= read -r sd_reset
IFS= read -r cost
IFS= read -r dur
} < <(read_fields)
now=$(date +%s)
# --- colors / glyphs ----------------------------------------------------------
BLUE=$'\033[94m'; YELLOW=$'\033[33m'; RED=$'\033[31m'; GREEN=$'\033[32m'
DIM=$'\033[2;37m'; RESET=$'\033[0m'
ORANGE=$'\033[38;5;173m'; EFFORTC=$'\033[38;5;179m'; CYAN=$'\033[1;36m'
K8S=$'\033[2;34m'; GITC=$'\033[31m'; GITN=$'\033[2;33m'; TIMEC=$'\033[1;33m'
FULL=${CC_SL_FULL:-●}; EMPTY=${CC_SL_EMPTY:-○}
Q1=${CC_SL_Q1:-◔}; HALF=${CC_SL_HALF:-◑}; Q3=${CC_SL_Q3:-◕}
is_set() { [ -n "$1" ] && [ "$1" != "null" ]; }
# --- line 1 -------------------------------------------------------------------
[ -z "$model" ] || [ "$model" = "null" ] && model="Claude"
cwd_tilde="${cwd/#$HOME/\~}"
line1="${ORANGE}${model}${RESET}"
is_set "$effort" && line1+=" ${EFFORTC}[${effort}]${RESET}"
is_set "$cwd_tilde" && line1+=" ${CYAN}${cwd_tilde}${RESET}"
if command -v kubectl >/dev/null 2>&1; then
kctx=$(kubectl config current-context 2>/dev/null)
if [ -n "$kctx" ]; then
kns=$(kubectl config view --minify -o "jsonpath={..namespace}" 2>/dev/null)
if [ -n "$kns" ]; then line1+=" ${K8S}⎈ ${kctx}(${kns})${RESET}"
else line1+=" ${K8S}⎈ ${kctx}${RESET}"; fi
fi
fi
if [ -n "$cwd" ] && git -C "$cwd" --no-optional-locks -c core.useBuiltinFSMonitor=false rev-parse --git-dir >/dev/null 2>&1; then
G=(git -C "$cwd" --no-optional-locks -c core.useBuiltinFSMonitor=false)
br=$("${G[@]}" symbolic-ref --short HEAD 2>/dev/null || "${G[@]}" rev-parse --short HEAD 2>/dev/null)
ch=$("${G[@]}" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ -n "$br" ]; then
if [ "${ch:-0}" -gt 0 ]; then line1+=" ${GITC}${br}${RESET} ${GITN}(+${ch})${RESET}"
else line1+=" ${GITC}${br}${RESET}"; fi
fi
fi
# --- line 2 -------------------------------------------------------------------
round() { printf '%.0f' "$1" 2>/dev/null; }
fmt_remaining() { # $1=sec $2=daily(1/0) — rounds to nearest unit
local sec=$1 daily=$2 h m d rh
[ "$sec" -lt 0 ] && sec=0
if [ "$daily" -eq 1 ]; then
h=$(( (sec + 1800) / 3600 )) # nearest hour
d=$(( h / 24 )); rh=$(( h % 24 ))
if [ "$d" -gt 0 ]; then
if [ "$rh" -gt 0 ]; then printf '%dd%dh' "$d" "$rh"; else printf '%dd' "$d"; fi
return
fi
# <1 day left: fall through to h/m so we never show "0h"
fi
m=$(( (sec + 30) / 60 )) # nearest minute
h=$(( m / 60 )); m=$(( m % 60 ))
if [ "$h" -gt 0 ]; then printf '%dh%dm' "$h" "$m"; else printf '%dm' "$m"; fi
}
render_window() { # $1=label $2=windowSec $3=units $4=daily $5=used $6=reset
local label=$1 win=$2 units=$3 daily=$4 used=$5 reset=$6
is_set "$reset" || return 0
reset=${reset%.*}
case "$reset" in ''|*[!0-9]*) return 0 ;; esac
local elapsed=$(( now - (reset - win) ))
[ "$elapsed" -lt 0 ] && elapsed=0
[ "$elapsed" -gt "$win" ] && elapsed=$win
local u; u=$(round "$used"); u=${u:-0}
[ "$u" -lt 0 ] && u=0; [ "$u" -gt 100 ] && u=100
# Dots track usage %, not the clock. Floor to the last fully-consumed quarter so a
# part never claims usage you haven't reached (each part = a quarter of one dot).
local maxq=$(( units * 4 ))
local quarters=$(( u * maxq / 100 ))
[ "$quarters" -gt "$maxq" ] && quarters=$maxq
local full=$(( quarters / 4 )) rem=$(( quarters % 4 ))
local partial=""
case "$rem" in
1) partial="$Q1" ;;
2) partial="$HALF" ;;
3) partial="$Q3" ;;
esac
local pcount=0; [ -n "$partial" ] && pcount=1
local empty=$(( units - full - pcount ))
# Color = burn pace: usage % vs the share of the window's time already elapsed.
# Just after a reset elapsed≈0, so the ratio explodes and even 1% reads as
# "too fast". Below PACE_FLOOR% used you can't exhaust the window regardless of
# pace, so stay blue and skip the unstable ratio entirely.
local col=$DIM pct=""
if is_set "$used"; then
if [ "$u" -lt "${CC_SL_PACE_FLOOR:-5}" ]; then col=$BLUE
elif [ $(( u * win )) -le $(( elapsed * 100 )) ]; then col=$BLUE
elif [ $(( 2 * u * win )) -le $(( 3 * elapsed * 100 )) ]; then col=$YELLOW
else col=$RED; fi
pct=" ${col}${u}%${RESET}"
fi
local dots="" i=0
while [ $i -lt $full ]; do dots+="$FULL"; i=$((i+1)); done
[ -n "$partial" ] && dots+="$partial"
i=0; while [ $i -lt $empty ]; do dots+="$EMPTY"; i=$((i+1)); done
printf '%s %s%s%s%s %s↻%s%s' "$label" "$col" "$dots" "$RESET" "$pct" "$DIM" "$(fmt_remaining $(( reset - now )) "$daily")" "$RESET"
}
ctxseg=""
if is_set "$ctx"; then
cpct=$(round "$ctx"); cpct=${cpct:-0}
[ "$cpct" -lt 0 ] && cpct=0; [ "$cpct" -gt 100 ] && cpct=100
filled=$(( (cpct + 5) / 10 )); [ "$filled" -gt 10 ] && filled=10
if [ "$cpct" -lt 60 ]; then cc=$GREEN; elif [ "$cpct" -le 80 ]; then cc=$YELLOW; else cc=$RED; fi
bar="" i=0
while [ $i -lt $filled ]; do bar+="█"; i=$((i+1)); done
while [ $i -lt 10 ]; do bar+="░"; i=$((i+1)); done
ctxseg="⛁ ${cc}${bar} ${cpct}%${RESET}"
fi
seg5=$(render_window "5h" 18000 5 0 "$fh_used" "$fh_reset")
seg7=$(render_window "7d" 604800 7 1 "$sd_used" "$sd_reset")
usage="$seg5"
[ -n "$seg7" ] && { [ -n "$usage" ] && usage="$usage | $seg7" || usage="$seg7"; }
stat=""
if is_set "$cost"; then
cf=$(printf '$%.2f' "$cost" 2>/dev/null)
[ "$cf" != '$0.00' ] && stat="$cf"
fi
if is_set "$dur"; then
ds=$(( ${dur%.*} / 1000 ))
if [ "$ds" -gt 0 ]; then
h=$(( ds / 3600 )); m=$(( (ds % 3600) / 60 ))
if [ "$h" -gt 0 ]; then d="${h}h${m}m"; else d="${m}m"; fi
[ -n "$stat" ] && stat="$stat ⏱ $d" || stat="⏱ $d"
fi
fi
# Time leads line 2, sitting under the model name on line 1.
line2="${TIMEC}$(date +%T)${RESET}"
[ -n "$ctxseg" ] && line2="$line2 | $ctxseg"
[ -n "$usage" ] && line2="$line2 | $usage"
[ -n "$stat" ] && line2="$line2 │ $stat"
printf '%s\n%s\n' "$line1" "$line2"
What it shows
- Model display name
- Effort level in brackets, when the session reports one
- Current working directory, with the home folder shortened to ~
- Kubernetes context and namespace, when kubectl is installed and a context is set
- Current git branch (or short commit hash when detached)
- Count of uncommitted git changes next to the branch name
- Current time in HH:MM:SS
- Context window usage as a 10-segment block bar with a percentage
- 5-hour rate limit usage as a 5-dot meter with a percentage and time until the window resets
- 7-day rate limit usage as a 7-dot meter with a percentage and time until the window resets
- Session cost in US dollars
- Session duration in hours and minutes
Requirements
- Bash (the script uses bash arrays and bash string substitution)
- awk, which does all the JSON parsing
- Standard Unix utilities: date, printf, wc, tr
- git is optional; the branch segment only appears when git is installed and the directory is a repository
- kubectl is optional; the Kubernetes segment only appears when kubectl is installed
- No jq, node, or bun required
- No network access; the script declares no network hosts and runs with network access off
- A terminal font covering plain Unicode symbols (● ◔ ◑ ◕ ○ █ ░ ⛁ ⎈ ↻ ⏱); no Nerd Font patches needed
Behavior notes
- In a brand-new session with no context or rate limit data, the bar and both dot meters disappear and line 2 shrinks to just the clock and session duration
- A cost of exactly $0.00 is hidden rather than shown
- The effort bracket disappears when the session JSON has no effort field, as in the Haiku scenario
- The 7-day meter is omitted when only the 5-hour limit is reported
- A dirty repository adds a (+N) change count after the branch name; a clean repository shows only the branch
- The context bar is green at 22% and 48%, yellow at 64%, and red at 91%; the source colors it green below 60%, yellow up to 80%, red above
- Dot meters fill in quarter steps, so 26% on the 5-dot meter renders one full dot plus a quarter dot
- Dot color tracks burn pace rather than fill: the 5-hour meter is yellow at 33% used with 3.5 hours left, but blue at 88% used with only 18 minutes left
- Reset countdowns switch units by window: the weekly window shows days and hours (↻2d1h, ↻5d) while the 5-hour window shows hours and minutes (↻2h7m, ↻18m)
- The branch comes from running git in the current directory, so a worktree session shows its checked-out branch and a non-git scratch directory shows no branch segment at all
- Vim mode, PR details, session name, agent name, and output style appear in the input JSON but never in the output
- If the model name is missing or null, the script falls back to the label Claude
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 │
╰──────────────────────────────────────────────────────╯
Single-line usage
87 copies
app · main · Opus-4.8 · effort high · ctx 22% · 5h 26% ↻2h6m · 7d 7% ↻2d0hCatppuccin Frappé
38 copies
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
Dreambase Flat
29 copies
████▊░░░░░░░░░░░░░░░░░ 22% of 200K · GOOD │ ↓ 44.0K ↑ 1.4K │ $0.41 │ ⏱ 10m 12s │ +128/-34 │ ◆ Opus 4.8 · main · 16:22
Color-Coded Context
25 copies
◆ Opus 4.8 │ ▪ app │ ⎇ main │ ◷ 16:20 │ ██▄░░░░░░░ 26% (44K/167K) │ 26% ↻2h6m │ 7% ⟳2d0h