{"slug": "always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline", "title": "Always-On Cost and Plan-Quota Visibility for Claude Code: Building a Statusline", "summary": "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.", "body_md": "In my last post, [\"Letting Claude Code improve itself autonomously — autopilot\"](https://zenn.dev/bokuwalily/articles/claude-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.\n\nNo auth, no endpoint calls. All you do is read the JSON that Claude Code passes to the statusline script over stdin with `jq`\n\n, 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.\n\nClaude 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**.\n\nThe \"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.\n\nEvery time Claude Code launches the statusline script, it streams JSON to stdin. Almost everything you need is already there.\n\n```\n# ~/.claude/scripts/statusline.sh\nINPUT=\"$(cat 2>/dev/null || echo '{}')\"\n\nj() { printf '%s' \"$INPUT\" | jq -r \"$1\" 2>/dev/null; }\n\nCTX=$(j '.context_window.used_percentage // empty')\nH5=$(j '.rate_limits.five_hour.used_percentage // empty')\nH5R=$(j '.rate_limits.five_hour.resets_at // empty')\nD7=$(j '.rate_limits.seven_day.used_percentage // empty')\nD7R=$(j '.rate_limits.seven_day.resets_at // empty')\nSESSION_USD=$(j '.cost.total_cost_usd // 0')\n```\n\n`.context_window.used_percentage`\n\nis a value Claude Code precomputes and hands you, so you don't have to count tokens yourself. `rate_limits`\n\nonly appears after a subscription user has received their first API response, so without the `// empty`\n\nfallback you'll get an error right at the start of a session.\n\n`rate_limits`\n\nonly appears \"after the first API response, and only on a subscription.\" Right after startup, fall back to a`--`\n\ndisplay; the numbers start showing once the first turn comes back.\n\nReset times arrive as epoch seconds, so convert them to local time with `date`\n\n.\n\n```\nclk()  { [ -n \"$1\" ] && date -r \"$1\" \"+%H:%M\" 2>/dev/null; }         # → HH:MM\nmdhm() { [ -n \"$1\" ] && date -r \"$1\" \"+%-m/%-d %H:%M\" 2>/dev/null; } # → M/D HH:MM\n```\n\nThe output is a fixed three-line structure.\n\n```\nline1: 📁 dir   ⌥ branch\nline2: 🤖 model·effort   🧠 ctx%   🕐 5h N% ⏪HH:MM   📅 7d N% ⏪M/D HH:MM\nline3: 💴 session ≈¥x   今日 ≈¥y   <health>\n```\n\nThe actual format calls look like this.\n\n```\nL2=$(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\" \\\n  \"$MODEL\" \"$EFF\" \"$(pct \"$CTX\")\" \"$(pct \"$H5\")\" \"$R5\" \"$(pct \"$D7\")\" \"$R7\")\nL3=$(printf \"${DIM}💴 session${R} %s   ${DIM}今日${R} %s  %s%b\" \\\n  \"$(yen \"$SESSION_USD\")\" \"$(yen \"$DAY_USD\")\" \"$HEALTH\" \"$PROJ_HEALTH_SEG\")\n```\n\nThe percentages are color-coded, so you can read the state without reading the numbers.\n\n``` php\n# pct -> color (green <50, yellow 50-79, red >=80)\npcolor() { local n=\"${1%%.*}\"; [ -z \"$n\" ] && { printf '%s' \"$DIM\"; return; }\n  if [ \"$n\" -ge 80 ] 2>/dev/null; then printf '%s' \"$RED\"\n  elif [ \"$n\" -ge 50 ] 2>/dev/null; then printf '%s' \"$YEL\"\n  else printf '%s' \"$GRN\"; fi; }\n```\n\n`SESSION_USD`\n\ncomes 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.\n\n```\nCCUSAGE=$(command -v ccusage 2>/dev/null || echo \"$HOME/.nvm/versions/node/v24.13.0/bin/ccusage\")\nDAY_CACHE=\"/tmp/cc-daily-cache.json\"\nNEED=true\nif [ -f \"$DAY_CACHE\" ]; then\n  AGE=$(($(date +%s) - $(stat -f %m \"$DAY_CACHE\" 2>/dev/null || echo 0)))\n  [ \"$AGE\" -lt 120 ] && NEED=false\nfi\n[ \"$NEED\" = true ] && ( \"$CCUSAGE\" daily --json 2>/dev/null > \"${DAY_CACHE}.tmp\" && mv \"${DAY_CACHE}.tmp\" \"$DAY_CACHE\" ) &\nDAY_USD=0\nif [ -f \"$DAY_CACHE\" ]; then\n  TODAY=$(date \"+%Y-%m-%d\")\n  DAY_USD=$(jq -r --arg d \"$TODAY\" '(.daily[] | select(.date==$d) | .totalCost) // (.daily[-1].totalCost) // 0' \"$DAY_CACHE\" 2>/dev/null)\nfi\n```\n\nThe key point is throwing it into a subshell with `&`\n\n. 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.\n\nThe yen conversion is a one-line awk using a rate set at the top.\n\n``` bash\nJPY_RATE=150   # Edit to taste / use $ instead.\n\nyen() { awk -v u=\"$1\" -v r=\"$JPY_RATE\" 'BEGIN{\n  v=u*r; n=sprintf(\"%.0f\", v); s=\"\"; len=length(n); c=0\n  for(i=len;i>=1;i--){ s=substr(n,i,1) s; c++; if(c%3==0 && i>1) s=\",\" s }\n  printf \"¥%s\", s }'; }\n```\n\nIt displays with comma separators every three digits, like `¥1,234`\n\n. If you prefer dollars, just change the spots where `yen`\n\nis called to use `$SESSION_USD`\n\ndirectly.\n\nAt the end of L3, I show automation liveness as a single 🟢/🟡/🔴/⚫ character. Because `automation-health.sh`\n\nis expensive to launch, this one uses a 5-minute cache.\n\n```\nHEALTH_CACHE=\"/tmp/cc-health-cache\"; HEALTH=\"\"\nif [ -f \"$HEALTH_CACHE\" ]; then\n  HAGE=$(($(date +%s) - $(stat -f %m \"$HEALTH_CACHE\" 2>/dev/null || echo 0)))\n  [ \"$HAGE\" -lt 300 ] && HEALTH=$(cat \"$HEALTH_CACHE\" 2>/dev/null)\nfi\nif [ -z \"$HEALTH\" ]; then\n  ( h=$(~/.claude/scripts/automation-health.sh 2>&1 | grep -oE \"ALL GREEN|WARN|FAIL\" | head -1)\n    case \"$h\" in\n      \"ALL GREEN\") echo \"🟢\" > \"$HEALTH_CACHE\" ;;\n      \"WARN\")      echo \"🟡\" > \"$HEALTH_CACHE\" ;;\n      \"FAIL\")      echo \"🔴\" > \"$HEALTH_CACHE\" ;;\n      *)           echo \"⚫\" > \"$HEALTH_CACHE\" ;;\n    esac ) &\n  HEALTH=\"·\"\nfi\n```\n\nWhen the cache has expired, it returns `·`\n\n(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.\n\nThe 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`\n\n.\n\nIt prefers ccusage's official block aggregation, but also computes its own aggregation from `~/.claude/logs/cost-log.jsonl`\n\n, calculating both and emitting the difference rate (`source_diff_pct`\n\n).\n\n```\n# ccusage が居れば5hブロックのoutput tokenを取る（transcript計算より公式）\nif command -v ccusage >/dev/null 2>&1; then\n  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)\n  # ... activeブロックを抽出してoutputTokens/costUSDを取り出す\n  CC_OUTPUT_TOK=\"${EXTRACTED%|*}\"\n  CC_COST_5H=\"${EXTRACTED#*|}\"\nfi\n```\n\nThe decision thresholds are set like this.\n\n```\nTHRESH_5H_WARN      = 800_000   # output tokens → warn\nTHRESH_5H_CRIT      = 1_200_000 # → critical\nTHRESH_WEEK_WARN    = 3000      # USD / 7d\nTHRESH_SESS_PER_DAY = 5         # 直近3d平均 > 5 → burst\n```\n\nIn `--short`\n\nmode it produces one-line output, which autopilot reads to make its call ahead of time.\n\n```\n# autopilot側のコードより（前作で紹介）\nBUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)\nif echo \"$BUDGET\" | grep -qE '🔴|critical|cap-near'; then\n  log \"ABORT: budget critical\"; exit 0\nfi\n```\n\nIn the autopilot post I described an incident where \"a negative remaining slipped through because only the label was checked.\" For that reason,\n\n`token-budget-advisor.sh`\n\nrecomputes the remaining amount numerically, separately from the label, and stops if it's 0 or below — a double check.\n\n```\n~/.claude/scripts/cost-summary.sh           # 7日分（デフォルト）\n~/.claude/scripts/cost-summary.sh 30        # 30日分\n~/.claude/scripts/cost-summary.sh today     # 今日だけ\n```\n\n`per day:`\n\nvisualizes usage with a Unicode bar graph.\n\n```\nbar = \"█\" * min(40, int(d[\"cost\"] * 4))\nprint(f\"    {day}: ${d['cost']:5.2f} ({d['n']}s) {bar}\")\n```\n\n`per model:`\n\nalso breaks down message counts by model. I use it to look back on \"what did I do on days with lots of Sonnet.\"\n\n`rate_limits`\n\ndoesn't exist right after a session starts`// empty`\n\n. A `--`\n\ndisplay is normal until the first turn returns`&`\n\nto make it async. Even when stale, the render never stops`&`\n\n. While refreshing, it just returns `·`\n\n`source_diff_pct`\n\nso cross-validation is always possible`stat -f %m`\n\nis macOS-only`stat -c %Y`\n\n. The script is written assuming macOS`rate_limits`\n\nand tried to hit an endpoint`/tmp/cc-statusline-last.json`\n\n) in from the start helps you catch this sooner\n\n```\n# デバッグ用。消してもよい\nprintf '%s' \"$INPUT\" > /tmp/cc-statusline-last.json 2>/dev/null || true\n```\n\n`.rate_limits.*`\n\n/ `.context_window.used_percentage`\n\n/ `.cost.total_cost_usd`\n\non stdin. No auth needed`token-budget-advisor.sh`\n\ncross-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](https://zenn.dev/bokuwalily/articles/github-scout-daily).\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline", "canonical_source": "https://dev.to/bokuwalily/always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline-205o", "published_at": "2026-07-13 05:00:04+00:00", "updated_at": "2026-07-13 05:14:09.533963+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "large-language-models"], "entities": ["Claude Code", "Claude Max", "jq", "ccusage"], "alternates": {"html": "https://wpnews.pro/news/always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline", "markdown": "https://wpnews.pro/news/always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline.md", "text": "https://wpnews.pro/news/always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline.txt", "jsonld": "https://wpnews.pro/news/always-on-cost-and-plan-quota-visibility-for-claude-code-building-a-statusline.jsonld"}}