cd /news/developer-tools/always-on-cost-and-plan-quota-visibi… · home topics developer-tools article
[ARTICLE · art-56897] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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

A developer built a statusline script for Claude Code that displays plan quota, context usage, and session cost in real time without authentication or API calls. The script reads JSON from stdin via jq, showing 5-hour and 7-day rate limits, context percentage, and daily cost converted to yen, with color-coded thresholds for at-a-glance monitoring.

read7 min views1 publishedJul 13, 2026

In my last post, "Letting Claude Code improve itself autonomously — autopilot", I covered how to keep autopilot from running away, but I glossed over the part about "surfacing your plan quota in the statusline" in a single paragraph. This time I'll walk through that implementation in full.

No auth, no endpoint calls. All you do is read the JSON that Claude Code passes to the statusline script over stdin with jq

, and you get your 5h/7d plan quota, context usage, and session cost plus today's running total (converted to yen) always visible in three lines.

Claude Max's API has soft rate limits. As your output tokens approach the cap within a 5-hour block or a 7-day window, the model's behavior changes, and once you burn through it, subsequent slots get skipped. The problem is that you usually can't see this consumption.

The "open a dashboard in a separate tab" approach dies within three days. If the numbers are always on your working screen, the remaining quota catches your eye without you having to think about it.

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

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

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.

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; }

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.

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.

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

).

if command -v ccusage >/dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  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.

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.

~/.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."

rate_limits

doesn't exist right after a session starts// empty

. A --

display is normal until the first turn returns&

to make it async. Even when stale, the render never stops&

. While refreshing, it just returns ·

source_diff_pct

so cross-validation is always possiblestat -f %m

is macOS-onlystat -c %Y

. The script is written assuming macOSrate_limits

and tried to hit an endpoint/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

.rate_limits.*

/ .context_window.used_percentage

/ .cost.total_cost_usd

on stdin. No auth neededtoken-budget-advisor.sh

cross-validates ccusage's official values against its own aggregation and feeds autopilot's run/stop decisionNext 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*

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/always-on-cost-and-p…] indexed:0 read:7min 2026-07-13 ·