{"slug": "fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost", "title": "Fully Custom Claude Code Status Line: A JSON-Driven 3-Line Readout of Quota, Cost, and Health", "summary": "A developer built a custom three-line status readout for Claude Code that displays plan quota, context usage, session cost, and automation health. The implementation reads JSON piped from stdin, eliminating the need for separate API calls, and caches the data to a file for fallback when stdin is empty. The status line helps the developer monitor consumption at a glance, preventing issues like running out of quota during overnight automated runs.", "body_md": "This is part of my \"Claude Code environment\" series. Last time I wrote about the [Codex worktree swarm](https://zenn.dev/bokuwalily/articles/codex-worktree-swarm), a setup that gets Codex and Claude Code collaborating. This time the topic is something far less flashy but that you look at every single turn: **customizing the status line**.\n\nClaude Code can print a single status line at the bottom of the window. By default it shows little more than the model name. Using the feature that lets you point `statusLine.command`\n\nin `settings.json`\n\nat an arbitrary shell script, I built a version that **formats your 5h/7d plan quota, context usage, session cost, and automation health into three lines** — and I'll walk through the whole implementation. The nice part is that the quota numbers **flow in directly from stdin**, so no API calls and no auth are required.\n\nOnce I started running `autopilot.sh`\n\novernight, I kept hitting the same accident: \"the 5h block quota is at zero by morning, so every job after that got SKIPPED.\" The issue was that **checking cost or quota meant running ccusage every time**. Because checking was a hassle, I stopped checking, and I noticed problems too late.\n\nThe ideal state is \"one glance at the screen tells me my current plan consumption.\" If I can just put that on Claude Code's status line, I don't even need to open a terminal.\n\n`settings.json`\n\nconfig: how JSON drives it\nLines 269–274 of `~/.claude/settings.json`\n\nare just this:\n\n```\n\"statusLine\": {\n  \"type\": \"command\",\n  \"command\": \"~/.claude/scripts/statusline.sh\",\n  \"padding\": 0\n}\n```\n\nWith `type: \"command\"`\n\n, every turn Claude Code launches that command and **pipes JSON into stdin**. Standard output is displayed verbatim as the status line.\n\nSo what's in that stdin? The actual fields are:\n\n| Path | Contents |\n|---|---|\n`.rate_limits.five_hour.used_percentage` |\n5h block usage rate (0–100) |\n`.rate_limits.five_hour.resets_at` |\nReset time (UNIX epoch) |\n`.rate_limits.seven_day.used_percentage` |\n7-day block usage rate |\n`.rate_limits.seven_day.resets_at` |\n7-day reset time (UNIX epoch) |\n`.context_window.used_percentage` |\nContext usage rate (pre-computed) |\n`.cost.total_cost_usd` |\nCurrent session cost (USD) |\n`.model.display_name` |\nModel name |\n`.effort.level` |\nEffort level |\n\nNote:`rate_limits`\n\nonly appearsafter the first API responsefor subscription members. Before the first turn it's empty, so the script needs to fall back to`--`\n\n.\n\n`statusline.sh`\n\n: reading stdin and falling back\nAt the top of the script it reads stdin and, if it's valid JSON, caches it to `/tmp/cc-statusline-last.json`\n\n.\n\n```\nRAW_INPUT=\"$(cat 2>/dev/null || true)\"\nif printf '%s' \"$RAW_INPUT\" | jq -e 'type == \"object\" and length > 0' >/dev/null 2>&1; then\n  INPUT=\"$RAW_INPUT\"\n  printf '%s' \"$INPUT\" > /tmp/cc-statusline-last.json 2>/dev/null || true\nelif [ -s /tmp/cc-statusline-last.json ]; then\n  INPUT=\"$(cat /tmp/cc-statusline-last.json 2>/dev/null || echo '{}')\"\nelse\n  INPUT='{}'\nfi\n```\n\nEven when stdin is empty (e.g. when another tool references it), it can read back from the cache. `claude-limits-segment.sh`\n\nis set up to receive this cache path via the `CLAUDE_STATUSLINE_JSON`\n\nenvironment variable:\n\n```\nSTATUS_JSON=\"${CLAUDE_STATUSLINE_JSON:-/tmp/cc-statusline-last.json}\"\n```\n\nHide the jq call behind a helper function:\n\n```\nj() { printf '%s' \"$INPUT\" | jq -r \"$1\" 2>/dev/null; }\n```\n\nFetching plan quota and context is just this:\n\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`pcolor()`\n\nconverts a number to an ANSI color:\n\n```\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\nApply this to all three: context, 5h, and 7d:\n\n```\nCTXC=$(pcolor \"$CTX\")\nC5=$(pcolor \"$H5\")\nC7=$(pcolor \"$D7\")\n```\n\nThe spec in the script's comments maps directly to the final output:\n\n```\nline1: 📁 dir   ⌥ branch\nline2: 🤖 model·effort   🧠 ctx%   🕐 5h N% ⏪reset   📅 7d N% ⏪reset\nline3: 💴 session ≈¥x   今日 ≈¥y   <health>\n```\n\nIn code:\n\n```\nL1=$(printf \"${DIM}📁${R} ${CYAN}%s${R}%s%s\" \"$DIRNAME\" \"$BR\" \"$WT\")\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\nprintf \"%b\\n%b\\n%b\" \"$L1\" \"$L2\" \"$L3\"\n```\n\nReset times are converted from epoch with `date -r`\n\n. The 5h uses `HH:MM`\n\n, and the 7d uses `M/D HH:MM`\n\n(since it can cross day boundaries):\n\n```\nclk()  { [ -n \"$1\" ] && date -r \"$1\" \"+%H:%M\" 2>/dev/null; }\nmdhm() { [ -n \"$1\" ] && date -r \"$1\" \"+%-m/%-d %H:%M\" 2>/dev/null; }\n```\n\nThe 5h block quota comes straight from stdin, but **today's total cost** requires an external command, `ccusage daily`\n\n. Calling it every turn feels slow, so I use a 2-minute cache plus background refresh:\n\n```\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\" ) &\n```\n\nThe `&`\n\nlaunches it in the background, while the foreground reads immediately from the cache. The display can lag by up to 2 minutes, but the status line never freezes. The atomic `tmp`\n\n→`mv`\n\nswap also prevents corruption from reading a half-written file.\n\n`automation-health.sh`\n\nchecks the success/failure of every launchd job, so it's expensive to start. Calling it every turn produces noticeable lag. The solution is the same pattern — **5-minute cache plus background refresh**:\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=\"·\"   # show a middle dot while updating\nfi\n```\n\nWhile the cache is valid, it just `cat`\n\ns the file. Zero subprocesses. If it's expired, it fires the refresh into the background with `&`\n\nand shows `·`\n\n(a middle dot) in the meantime. By the next turn the cache is updated and it switches to an icon.\n\n`project-health-latest.md`\n\n(the project health report) follows the same philosophy — resolve everything by just reading a file:\n\n```\nPH_FILE=\"$HOME/.claude/logs/project-health-latest.md\"\nPH_WARN=$(grep -oE '^## Warnings \\([0-9]+\\)' \"$PH_FILE\" 2>/dev/null | grep -oE '[0-9]+' | head -1)\n```\n\nIt picks up the warning count with grep, showing 🟢 if 0 and 🟡/🔴 if there are any. No external process is started.\n\nNote:\"The status line script is called every turn.\" Startup cost adds up. The rule is:the only things done synchronously are; everything else is offloaded to a cache or the background.`jq`\n\nand ANSI formatting\n\n`claude-limits-segment.sh`\n\n: carving out a component for other tools\nSeparately from `statusline.sh`\n\n, I also keep a small script that returns just the plan quota. It's used by things like `autopilot.sh`\n\nto check \"does the plan have room right now?\":\n\n```\nSTATUS_JSON=\"${CLAUDE_STATUSLINE_JSON:-/tmp/cc-statusline-last.json}\"\n\nh5=$(jq_field '.rate_limits.five_hour.used_percentage')\nh5r=$(jq_field '.rate_limits.five_hour.resets_at')\nd7=$(jq_field '.rate_limits.seven_day.used_percentage')\nd7r=$(jq_field '.rate_limits.seven_day.resets_at')\n\nstale=''\n[ \"$age\" -gt 600 ] && stale='*'   # mark caches older than 10 minutes\n\nprintf '🕐 5h %s' \"$(pct \"$h5\")\"\n[ -n \"$r5\" ] && printf ' ⏪%s' \"$r5\"\nprintf ' · 📅 7d %s' \"$(pct \"$d7\")\"\n[ -n \"$r7\" ] && printf ' ⏪%s' \"$r7\"\nprintf '%s' \"$stale\"\n```\n\nIt references the very same `/tmp/cc-statusline-last.json`\n\nthat `statusline.sh`\n\nwrote. If the cache is older than 10 minutes, it appends a `*`\n\nto make staleness visible.\n\n`rate_limits`\n\nalways looks empty`// empty`\n\nto fall back on null, `jq`\n\nreturns an empty string and `pcolor`\n\ndies.`date -r`\n\nis the BSD version, with different syntax from Linux's `date -d @epoch`\n\n. `stat -f %m`\n\nis the same story. Using this on Linux requires rewriting.`ccusage`\n\nevery turn freezes the terminal`tmp`\n\nfile is also essential.`automation-health.sh`\n\nis heavy to start and causes lag`·`\n\nwhile updating.`printf \"%b\"`\n\nfor the status line output expands the escapes correctly. `echo -e`\n\nis not portable.`git -C \"$REAL_CWD\"`\n\n, it references the main repo instead.`statusLine.command`\n\nin `settings.json`\n\ndelivers JSON stdin every turn.`rate_limits`\n\n, `context_window`\n\n, and `cost.total_cost_usd`\n\ncan be read straight from stdin with no API call.`pcolor()`\n\nwith two thresholds at 50% and 80%.`jq`\n\nand string formatting only.`tmp`\n\n→`mv`\n\nswap prevents cache reads from being corrupted.`claude-limits-segment.sh`\n\n) makes it easy to reuse from other automation.Next time I plan to write about the internals of the automation health check (`automation-health.sh`\n\n) shown on this status line — a setup that lists the liveness of launchd jobs, script exit codes, and the last successful run time.\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/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost", "canonical_source": "https://dev.to/bokuwalily/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost-and-health-56d6", "published_at": "2026-07-18 05:00:05+00:00", "updated_at": "2026-07-18 05:32:20.422119+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Claude Code", "Codex"], "alternates": {"html": "https://wpnews.pro/news/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost", "markdown": "https://wpnews.pro/news/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost.md", "text": "https://wpnews.pro/news/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost.txt", "jsonld": "https://wpnews.pro/news/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost.jsonld"}}