# Fully Custom Claude Code Status Line: A JSON-Driven 3-Line Readout of Quota, Cost, and Health

> Source: <https://dev.to/bokuwalily/fully-custom-claude-code-status-line-a-json-driven-3-line-readout-of-quota-cost-and-health-56d6>
> Published: 2026-07-18 05:00:05+00:00

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**.

Claude 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`

in `settings.json`

at 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.

Once I started running `autopilot.sh`

overnight, 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.

The 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.

`settings.json`

config: how JSON drives it
Lines 269–274 of `~/.claude/settings.json`

are just this:

```
"statusLine": {
  "type": "command",
  "command": "~/.claude/scripts/statusline.sh",
  "padding": 0
}
```

With `type: "command"`

, every turn Claude Code launches that command and **pipes JSON into stdin**. Standard output is displayed verbatim as the status line.

So what's in that stdin? The actual fields are:

| Path | Contents |
|---|---|
`.rate_limits.five_hour.used_percentage` |
5h block usage rate (0–100) |
`.rate_limits.five_hour.resets_at` |
Reset time (UNIX epoch) |
`.rate_limits.seven_day.used_percentage` |
7-day block usage rate |
`.rate_limits.seven_day.resets_at` |
7-day reset time (UNIX epoch) |
`.context_window.used_percentage` |
Context usage rate (pre-computed) |
`.cost.total_cost_usd` |
Current session cost (USD) |
`.model.display_name` |
Model name |
`.effort.level` |
Effort level |

Note:`rate_limits`

only appearsafter the first API responsefor subscription members. Before the first turn it's empty, so the script needs to fall back to`--`

.

`statusline.sh`

: reading stdin and falling back
At the top of the script it reads stdin and, if it's valid JSON, caches it to `/tmp/cc-statusline-last.json`

.

```
RAW_INPUT="$(cat 2>/dev/null || true)"
if printf '%s' "$RAW_INPUT" | jq -e 'type == "object" and length > 0' >/dev/null 2>&1; then
  INPUT="$RAW_INPUT"
  printf '%s' "$INPUT" > /tmp/cc-statusline-last.json 2>/dev/null || true
elif [ -s /tmp/cc-statusline-last.json ]; then
  INPUT="$(cat /tmp/cc-statusline-last.json 2>/dev/null || echo '{}')"
else
  INPUT='{}'
fi
```

Even when stdin is empty (e.g. when another tool references it), it can read back from the cache. `claude-limits-segment.sh`

is set up to receive this cache path via the `CLAUDE_STATUSLINE_JSON`

environment variable:

```
STATUS_JSON="${CLAUDE_STATUSLINE_JSON:-/tmp/cc-statusline-last.json}"
```

Hide the jq call behind a helper function:

```
j() { printf '%s' "$INPUT" | jq -r "$1" 2>/dev/null; }
```

Fetching plan quota and context is just this:

```
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')
```

`pcolor()`

converts a number to an ANSI color:

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

Apply this to all three: context, 5h, and 7d:

```
CTXC=$(pcolor "$CTX")
C5=$(pcolor "$H5")
C7=$(pcolor "$D7")
```

The spec in the script's comments maps directly to the final output:

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

In code:

```
L1=$(printf "${DIM}📁${R} ${CYAN}%s${R}%s%s" "$DIRNAME" "$BR" "$WT")
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")

printf "%b\n%b\n%b" "$L1" "$L2" "$L3"
```

Reset times are converted from epoch with `date -r`

. The 5h uses `HH:MM`

, and the 7d uses `M/D HH:MM`

(since it can cross day boundaries):

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

The 5h block quota comes straight from stdin, but **today's total cost** requires an external command, `ccusage daily`

. Calling it every turn feels slow, so I use a 2-minute cache plus background refresh:

```
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" ) &
```

The `&`

launches 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`

→`mv`

swap also prevents corruption from reading a half-written file.

`automation-health.sh`

checks 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**:

```
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="·"   # show a middle dot while updating
fi
```

While the cache is valid, it just `cat`

s the file. Zero subprocesses. If it's expired, it fires the refresh into the background with `&`

and shows `·`

(a middle dot) in the meantime. By the next turn the cache is updated and it switches to an icon.

`project-health-latest.md`

(the project health report) follows the same philosophy — resolve everything by just reading a file:

```
PH_FILE="$HOME/.claude/logs/project-health-latest.md"
PH_WARN=$(grep -oE '^## Warnings \([0-9]+\)' "$PH_FILE" 2>/dev/null | grep -oE '[0-9]+' | head -1)
```

It picks up the warning count with grep, showing 🟢 if 0 and 🟡/🔴 if there are any. No external process is started.

Note:"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`

and ANSI formatting

`claude-limits-segment.sh`

: carving out a component for other tools
Separately from `statusline.sh`

, I also keep a small script that returns just the plan quota. It's used by things like `autopilot.sh`

to check "does the plan have room right now?":

```
STATUS_JSON="${CLAUDE_STATUSLINE_JSON:-/tmp/cc-statusline-last.json}"

h5=$(jq_field '.rate_limits.five_hour.used_percentage')
h5r=$(jq_field '.rate_limits.five_hour.resets_at')
d7=$(jq_field '.rate_limits.seven_day.used_percentage')
d7r=$(jq_field '.rate_limits.seven_day.resets_at')

stale=''
[ "$age" -gt 600 ] && stale='*'   # mark caches older than 10 minutes

printf '🕐 5h %s' "$(pct "$h5")"
[ -n "$r5" ] && printf ' ⏪%s' "$r5"
printf ' · 📅 7d %s' "$(pct "$d7")"
[ -n "$r7" ] && printf ' ⏪%s' "$r7"
printf '%s' "$stale"
```

It references the very same `/tmp/cc-statusline-last.json`

that `statusline.sh`

wrote. If the cache is older than 10 minutes, it appends a `*`

to make staleness visible.

`rate_limits`

always looks empty`// empty`

to fall back on null, `jq`

returns an empty string and `pcolor`

dies.`date -r`

is the BSD version, with different syntax from Linux's `date -d @epoch`

. `stat -f %m`

is the same story. Using this on Linux requires rewriting.`ccusage`

every turn freezes the terminal`tmp`

file is also essential.`automation-health.sh`

is heavy to start and causes lag`·`

while updating.`printf "%b"`

for the status line output expands the escapes correctly. `echo -e`

is not portable.`git -C "$REAL_CWD"`

, it references the main repo instead.`statusLine.command`

in `settings.json`

delivers JSON stdin every turn.`rate_limits`

, `context_window`

, and `cost.total_cost_usd`

can be read straight from stdin with no API call.`pcolor()`

with two thresholds at 50% and 80%.`jq`

and string formatting only.`tmp`

→`mv`

swap prevents cache reads from being corrupted.`claude-limits-segment.sh`

) 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`

) shown on this status line — a setup that lists the liveness of launchd jobs, script exit codes, and the last successful run time.

*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.

Follow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*
