DEV Community

Always-On Cost and Plan-Quota Visibility for Claude Code: Building a Statusline

Values you can pull straight from stdin

Every time Claude Code launches the statusline script, it streams JSON to stdin. Almost everything you need is already there.

# ~/.claude/scripts/statusline.sh
INPUT="$(cat 2>/dev/null || echo '{}')"
j() { printf '%s' "$INPUT" | jq -r "$1" 2>/dev/null; }

CTX=$(j '.context_window.used_percentage // empty')
H5=$(j '.rate_limits.five_hour.used_percentage // empty')
H5R=$(j '.rate_limits.five_hour.resets_at // empty')
D7=$(j '.rate_limits.seven_day.used_percentage // empty')
D7R=$(j '.rate_limits.seven_day.resets_at // empty')
SESSION_USD=$(j '.cost.total_cost_usd // 0')

.context_window.used_percentage is a value Claude Code precomputes and hands you, so you don't have to count tokens yourself. rate_limits only appears after a subscription user has received their first API response, so without the // empty fallback you'll get an error right at the start of a session.

rate_limits only appears "after the first API response, and only on a subscription." Right after startup, fall back to a -- display; the numbers start showing once the first turn comes back.

Reset times arrive as epoch seconds, so convert them to local time with date.

clk() { [ -n "$1" ] && date -r "$1" "+%H:%M" 2>/dev/null; }   # → HH:MM
mdhm() { [ -n "$1" ] && date -r "$1" "+%-m/%-d %H:%M" 2>/dev/null; }  # → M/D HH:MM

Three-line layout

The output is a fixed three-line structure:

  • line1: 📁 dir ⌥ branch
  • line2: 🤖 model·effort 🧠 ctx% 🕐 5h N% ⏪HH:MM 📅 7d N% ⏪M/D HH:MM
  • line3: 💴 session ≈¥x 今日 ≈¥y <health>

The actual format calls look like this:

L2=$(printf "${MAG}🤖 %s%s${R} ${DIM}·${R} ${CTXC}🧠 ctx %s${R} ${DIM}·${R} ${C5}🕐 5h %s${R}%b ${DIM}·${R} ${C7}📅 7d %s${R}%b" \
  "$MODEL" "$EFF" "$(pct "$CTX")" "$(pct "$H5")" "$R5" "$(pct "$D7")" "$R7")

L3=$(printf "${DIM}💴 session${R} %s ${DIM}今日${R} %s %s%b" \
  "$(yen "$SESSION_USD")" "$(yen "$DAY_USD")" "$HEALTH" "$PROJ_HEALTH_SEG")

The percentages are color-coded, so you can read the state without reading the numbers.

# pct -> color (green <50, yellow 50-79, red >=80)
pcolor() {
  local n="${1%%.*}"; [ -z "$n" ] && { printf '%s' "$DIM"; return; }
  if [ "$n" -ge 80 ] 2>/dev/null; then printf '%s' "$RED"
  elif [ "$n" -ge 50 ] 2>/dev/null; then printf '%s' "$YEL"
  else printf '%s' "$GRN"; fi;
}

Getting today's cost with a 2-minute cache

SESSION_USD comes straight from stdin in real time, but "today's total cost" requires ccusage's daily aggregation. Calling it every time would stall the statusline render, so I use a 2-minute cache plus a background refresh.

CCUSAGE=$(command -v ccusage 2>/dev/null || echo "$HOME/.nvm/versions/node/v24.13.0/bin/ccusage")
DAY_CACHE="/tmp/cc-daily-cache.json"
NEED=true
if [ -f "$DAY_CACHE" ]; then
  AGE=$(( $(date +%s) - $(stat -f %m "$DAY_CACHE" 2>/dev/null || echo 0) ))
  [ "$AGE" -lt 120 ] && NEED=false
fi
[ "$NEED" = true ] && (
  "$CCUSAGE" daily --json 2>/dev/null > "${DAY_CACHE}.tmp" && mv "${DAY_CACHE}.tmp" "$DAY_CACHE"
) &
DAY_USD=0
if [ -f "$DAY_CACHE" ]; then
  TODAY=$(date "+%Y-%m-%d")
  DAY_USD=$(jq -r --arg d "$TODAY" '(.daily[] | select(.date==$d) | .totalCost) // (.daily[-1].totalCost) // 0' "$DAY_CACHE" 2>/dev/null)
fi

The key point is throwing it into a subshell with &. Even when the cache has expired, the statusline returns the old value at that instant and runs ccusage in the background. A number that's 2 minutes stale causes no practical problems in real use.

The yen conversion is a one-line awk using a rate set at the top.

JPY_RATE=150  # Edit to taste / use $ instead.
yen() {
  awk -v u="$1" -v r="$JPY_RATE" 'BEGIN{
    v=u*r; n=sprintf("%.0f", v); s=""; len=length(n); c=0
    for(i=len;i>=1;i--){ s=substr(n,i,1) s; c++; if(c%3==0 && i>1) s="," s }
    printf "¥%s", s
  }'
}

It displays with comma separators every three digits, like ¥1,234. If you prefer dollars, just change the spots where yen is called to use $SESSION_USD directly.

A one-character health indicator (5-minute cache)

At the end of L3, I show automation liveness as a single 🟢/🟡/🔴/⚫ character. Because automation-health.sh is expensive to launch, this one uses a 5-minute cache.

HEALTH_CACHE="/tmp/cc-health-cache"; HEALTH=""
if [ -f "$HEALTH_CACHE" ]; then
  HAGE=$(( $(date +%s) - $(stat -f %m "$HEALTH_CACHE" 2>/dev/null || echo 0) ))
  [ "$HAGE" -lt 300 ] && HEALTH=$(cat "$HEALTH_CACHE" 2>/dev/null)
fi
if [ -z "$HEALTH" ]; then
  (
    h=$(~/.claude/scripts/automation-health.sh 2>&1 | grep -oE "ALL GREEN|WARN|FAIL" | head -1)
    case "$h" in
      "ALL GREEN") echo "🟢" > "$HEALTH_CACHE" ;;
      "WARN")      echo "🟡" > "$HEALTH_CACHE" ;;
      "FAIL")      echo "🔴" > "$HEALTH_CACHE" ;;
      *)           echo "⚫" > "$HEALTH_CACHE" ;;
    esac
  ) &
  HEALTH="·"
fi

When the cache has expired, it returns · (a dot) while kicking off a background refresh. If the cache is ready by the next render cycle, it switches to the emoji. The statusline render must never block under any circumstances - that's the core of the design.

token-budget-advisor.sh: precise cross-validation

The statusline display is meant to visualize "roughly where I am right now." For autopilot to decide whether to "run or stop," you need precision, and that role falls to token-budget-advisor.sh. It prefers ccusage's official block aggregation, but also computes its own aggregation from ~/.claude/logs/cost-log.jsonl, calculating both and emitting the difference rate (source_diff_pct).

# ccusage が居れば5hブロックのoutput tokenを取る(transcript計算より公式)
if command -v ccusage > /dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  # ... activeブロックを抽出してoutputTokens/costUSDを取り出す
  CC_OUTPUT_TOK="${EXTRACTED%|*}"
  CC_COST_5H="${EXTRACTED#*|}"
fi

The decision thresholds are set like this:

THRESH_5H_WARN=800_000      # output tokens → warn
THRESH_5H_CRIT=1_200_000    # → critical
THRESH_WEEK_WARN=3000       # USD / 7d
THRESH_SESS_PER_DAY=5       # 直近3d平均 > 5 → burst

In --short mode it produces one-line output, which autopilot reads to make its call ahead of time.

# autopilot側のコードより(前作で紹介)
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '🔴|critical|cap-near'; then
  log "ABORT: budget critical"; exit 0
fi

In the autopilot post I described an incident where "a negative remaining slipped through because only the label was checked." For that reason, token-budget-advisor.sh recomputes the remaining amount numerically, separately from the label, and stops if it's 0 or below - a double check.

cost-summary.sh: aggregation by period

~/.claude/scripts/cost-summary.sh       # 7日分(デフォルト)
~/.claude/scripts/cost-summary.sh 30    # 30日分
~/.claude/scripts/cost-summary.sh today # 今日だけ

per day: visualizes usage with a Unicode bar graph.

bar = "█" * min(40, int(d["cost"] * 4))
print(f"{day}: ${d['cost']:5.2f} ({d['n']}s) {bar}")

per model: also breaks down message counts by model. I use it to look back on "what did I do on days with lots of Sonnet."

Pitfalls I hit

  • rate_limits doesn't exist right after a session starts → fall back with // empty. A -- display is normal until the first turn returns
  • ccusage daily blocks the statusline → 120-second cache plus & to make it async. Even when stale, the render never stops
  • The health check is heavy every time → 5-minute cache plus &. While refreshing, it just returns ·
  • I didn't notice the divergence between ccusage and my own aggregation → I emit source_diff_pct so cross-validation is always possible
  • stat -f %m is macOS-only → on Linux, change it to stat -c %Y. The script is written assuming macOS
  • I mistakenly thought there was no rate_limits and tried to hit an endpoint → putting a line that dumps the stdin JSON to a debug log (/tmp/cc-statusline-last.json) in from the start helps you catch this sooner
# デバッグ用。消してもよい
printf '%s' "$INPUT" > /tmp/cc-statusline-last.json 2>/dev/null || true

Wrap-up

  • Claude Code's statusline script receives .rate_limits.* / .context_window.used_percentage / .cost.total_cost_usd on stdin. No auth needed
  • Three lines give you an always-on view of "branch / 5h·7d·ctx% / session + today's cost in yen plus health"
  • Today's cost uses a 2-minute cache plus background refresh so it never blocks
  • The one-character health indicator is lightened with a 5-minute cache
  • token-budget-advisor.sh cross-validates ccusage's official values against its own aggregation and feeds autopilot's run/stop decision

Next up, building on this statusline and autopilot's monitoring, I'm writing about having Claude Code patrol GitHub every morning to automatically score useful OSS and skills - Having Claude Code patrol GitHub every morning - auto-scoring useful OSS and skills.

Written by Lily - I ship iOS apps and automate my content stack with Claude Code. Follow along: Portfolio · X · GitHub

Comments

No comments yet. Start the discussion.