# A Blank Dashboard and a Fake 2.5M-Token Reading: Building a Token Fuel Gauge for Claude Code

> Source: <https://dev.to/bokuwalily/a-blank-dashboard-and-a-fake-25m-token-reading-building-a-token-fuel-gauge-for-claude-code-36m6>
> Published: 2026-08-25 11:00:04+00:00

A side hustle I started in college went from ¥100k a month to ¥600k once I was running several at the same time. A layoff took it to zero. Then I spent six months building an autonomous setup around Claude Code, and today it runs at ¥1.2M a month in revenue. The core of that setup isn't "writing code" — it's **stacking up mechanisms that let me notice things before they fall apart**. This post opens up the complete wiring of one of them: a "token fuel gauge" that warns me before a 5-hour block burns out.

Claude Code (MAX plan) has usage blocks that reset every 5 hours. The problem is that **consumption is invisible from the outside**.

There's no remaining-capacity bar like the browser version has. When you're running a Claude Code session in a terminal, nothing on screen changes as you approach the ceiling of the 5-hour block. Responses don't get slower, and no error appears. It's just that **output quality quietly, gradually degrades**.

I noticed this one night when I asked for a refactor of an automation script. Same prompt, but the output was clearly thinner than when I'd run it that morning. Parts of the code were abbreviated, and error handling had dropped out. Checking later with `ccusage`

, that session's output tokens had already passed 800k. Right on the edge of the 800k threshold.

The problem was that I had no way to know that in real time, while working.

The more conscientious you are, the more you think "I'll just be more careful about how I use it." But that mindset can't beat the mechanics. The 5-hour count accumulates unconsciously, and the more focused you are, the faster it burns.

I chose the opposite approach: **automate the monitoring and embed the state permanently in the status line**. That drops the cognitive cost of "checking how much is left" to zero. You don't have to look on purpose — it's enough that a number sits somewhere your eyes pass over.

This is the same design philosophy as a fuel gauge. Nobody pops the hood and measures the oil level every time they drive. There's a gauge on the dashboard, so a glance is enough to make a judgment. Claude Code's 5-hour block just needs the same structure.

Here are the three things we build in this article.

`token-budget-advisor.sh`

`ccusage`

and `cost-log.jsonl`

— and emits a three-level verdict (🟢 ok / 🟡 warn / 🔴 critical)`--short`

mode`dashboard.sh`

Once it's done, every time you open a terminal you'll see something like this in the status line.

```
budget: 🟢 OK (5h:312k tok $1.2 / 7d:$48)
```

Or the color changes automatically as you approach the ceiling.

```
budget: 🟡 burst (5h:843k tok $3.1 / 7d:$92)
budget: 🔴 cap-near (5h:1231k tok $4.8 / 7d:$134)
```

The moment that enters your field of vision, the decision to "push the heavy work into the next block" becomes natural.

This system picks up information from two data sources and consolidates them into a single script.

```
┌─────────────────────────────────────────────────────┐
│  データソース層                                       │
│                                                     │
│  ccusage blocks --json ──────────────────┐          │
│  (アクティブブロックの公式出力トークン数)    │          │
│                                           ├─► token-budget-advisor.sh
│  ~/.claude/logs/cost-log.jsonl ──────────┘          │
│  (session_id × transcript ごとの累積コスト)           │
└─────────────────────────────────────────────────────┘
                         │
              ┌──────────▼──────────┐
              │  判定エンジン (Python) │
              │                     │
              │  5h output tokens   │
              │  ├ ≥ 1,200,000 → 🔴 critical
              │  ├ ≥   800,000 → 🟡 warn    
              │  └ <   800,000 → 🟢 ok      
              │                     │
              │  7d cost (USD)      │
              │  └ ≥ $3,000  → 🟡 warn      
              │                     │
              │  直近3日 セッション数  │
              │  └ 平均 > 5/day → burst     
              └──────────┬──────────┘
                         │
              ┌──────────▼──────────┐
              │  出力モード           │
              │                     │
              │  (引数なし) → JSON詳細 │
              │  --short   → 1行サマリ │
              └──────────┬──────────┘
                         │
              ┌──────────▼──────────┐
              │  dashboard.sh       │
              │  （日次自動更新）     │
              │                     │
              │  ## 💰 Cost (7d)    │
              │    budget: [--short] │
              └─────────────────────┘
```

This is an important implementation decision.

** ccusage blocks --json** is active-block data output by Claude Code's official CLI tool. It reflects the token count of the currently running block most accurately. However, you can't get data in environments where

`ccusage`

isn't installed, or when no block is active.** ~/.claude/logs/cost-log.jsonl** is the cost log Claude Code generates automatically. For each combination of session ID and transcript, it records the cumulative cost and output token count. Since this doesn't depend on

`ccusage`

, it always works as a fallback.The script's implementation has this priority order.

```
# ccusage が居れば 5h block の output token を取る (transcript 計算より公式)
CC_OUTPUT_TOK=""
CC_COST_5H=""
if command -v ccusage >/dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  if [ -n "$CC_JSON" ]; then
    EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    active = [b for b in d.get('blocks', []) if b.get('isActive')]
    if active:
        b = active[0]
        tc = b.get('tokenCounts', {}) or {}
        out = int(tc.get('outputTokens', 0))
        cost = float(b.get('costUSD', 0))
        print(f'{out}|{cost}')
    else:
        print('|')
...
```

It pulls out only the active block and receives `outputTokens`

and `costUSD`

pipe-delimited. Then, when combining with the aggregation result from `cost-log.jsonl`

, the ccusage values take priority (lines 127–131 of the code).

```
# ccusage の値が有効ならそちらを優先 (transcript 計算より信頼できる)
own_out_5h = out_5h
if cc_out is not None and cc_out > 0:
    out_5h = cc_out
if cc_cost is not None and cc_cost > 0:
    cost_5h = cc_cost
```

The result of this design is a fallback structure that **works even if either data source is missing**. If ccusage isn't usable, aggregation runs on cost-log.jsonl alone and the script exits 0 (fail-open policy).

This file has one trap: **multiple lines are recorded for the same session**. Because Claude Code writes to the log incrementally during a session, many intermediate aggregates remain from before the final token count was settled. Naively summing all lines causes double counting.

The "only take the last line" logic below avoids that (lines 100–111 of the code).

```
# cost-log は session_id × transcript ごとに累積値で書かれる仕様。
# 最新行のみ採用するため、(session_id, transcript) で最終行を取り直す。
latest = {}
with open(log_path) as f:
    for line in f:
        try:
            r = json.loads(line)
            t = datetime.datetime.fromisoformat(r["ts"])
        except Exception:
            continue
        key = (r.get("session_id", ""), r.get("transcript", ""))
        prev = latest.get(key)
        if (prev is None) or (t > prev[0]):
            latest[key] = (t, r)
```

It builds a dictionary keyed on the `(session_id, transcript)`

pair and keeps overwriting whenever a line has a newer timestamp. By the time the loop ends, what's left in `latest`

is only the final settled value for each session/transcript.

The values extracted through this aggregation are what feed the three-level threshold check.

There are four thresholds (lines 139–142 of the code).

```
THRESH_5H_WARN      = 800_000    # output tokens
THRESH_5H_CRIT      = 1_200_000
THRESH_WEEK_WARN    = 3000       # USD
THRESH_SESS_PER_DAY = 5
```

Output tokens in the 5-hour block **go to warn past 800k** and **critical past 1.2M**. Weekly cost goes to **warn past $3,000**. On top of that, if the session average over the last 3 days **exceeds 5 per day, a burst flag** is raised.

```
if out_5h >= THRESH_5H_CRIT:
    s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
    s5 = "warn"
else:
    s5 = "ok"

# 集中作業判定: 直近 3 日で平均 > 5 sess/day
recent_days = sorted(by_day.keys())[-3:]
avg_sess = sum(by_day[d] for d in recent_days) / max(1, len(recent_days))
burst = avg_sess > THRESH_SESS_PER_DAY
```

`burst`

functions as a "burnout forecast." Even when the absolute token count is still under the threshold, a high session frequency means consumption is that much faster. It works as a leading signal: you're fine now, but there's a good chance you'll cross into warn before the night is out.

`--short`

mode and folding it into dashboard.sh
The detailed JSON output is handy while debugging, but it's far too long to embed in a status line. Pass the `--short`

argument and you get a one-line summary.

```
result = {
    ...
    "_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",
}
```

The icon evaluates the 5-hour status with top priority (lines 173–181 of the code).

```
if s5 == "critical":
    icon, label = "🔴", "cap-near"
elif s5 == "warn" or sw == "warn":
    icon, label = "🟡", "burst"
elif burst:
    icon, label = "🟡", "burst"
else:
    icon, label = "🟢", "OK"
```

Running in `--short`

mode gives output like this.

```
🟢 OK (5h:312k tok $1.2 / 7d:$48)
```

`dashboard.sh`

pulls that single line in like so (line 79 of the script).

```
echo "## 💰 Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo "  budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
```

`dashboard.sh`

runs daily via cron and auto-updates `~/.claude/dashboard.md`

. In other words, **the next time you open the dashboard, yesterday's fuel-consumption summary has already been written into it**. The morning after a heavy session, one look at the dashboard tells you "yesterday went all the way to warn."

Real-time status-line integration is covered in more detail in the next chapter.

`set -u`

A single line at the top of the script packs in the whole design philosophy.

```
set -u  # -e は外す: fail-open 方針
```

Dropping `-e`

(exit immediately on error) is intentional. On line 79, `dashboard.sh`

calls the script inside a command substitution like this.

```
echo "  budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
```

If a subcommand inside a command substitution exits 1, under `set -e`

the calling shell itself dies. `dashboard.sh`

runs every morning via cron and updates several sections at once — Health, Cost, Hook latency, and more. Having the entire dashboard go blank every time `token-budget-advisor.sh`

dies from a ccusage path mismatch or a missing log is a problem.

So I set up `fail_open()`

.

```
fail_open() {
  if [ "$MODE" = "--short" ]; then
    echo "⚫ n/a"
  else
    printf '{"5h_status":"unknown","weekly_status":"unknown","advice":"%s"}\n' "${1:-no data}"
  fi
  exit 0
}

[ -f "$LOG" ] || fail_open "cost-log.jsonl not found"
```

In `--short`

mode it prints `⚫ n/a`

and finishes with `exit 0`

. The dashboard shows `budget: ⚫ n/a`

, but the state "data couldn't be fetched" remains on the page as text. That's far easier to debug than a silent blank.

```
if command -v ccusage >/dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  if [ -n "$CC_JSON" ]; then
    EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "
...
" 2>/dev/null || echo "|")
    CC_OUTPUT_TOK="${EXTRACTED%|*}"
    CC_COST_5H="${EXTRACTED#*|}"
  fi
fi
```

There are three layers.

**Layer 1**: existence check with `command -v ccusage >/dev/null 2>&1`

. In a launchd environment, PATH is only `/usr/bin:/bin:/usr/sbin:/sbin`

, so ccusage under nvm isn't visible. Skipping here means nothing after it is touched at all.

**Layer 2**: `ccusage blocks --json 2>/dev/null || true`

. This covers the case where ccusage exists but spits out some error (bad JSON, network problems). `|| true`

guarantees exit 0, and `CC_JSON`

becomes an empty string.

**Layer 3**: `python3 -c "..." 2>/dev/null || echo "|"`

. Even if the Python parse fails, it returns the fallback string `|`

. Because the following bash parameter expansions `"${EXTRACTED%|*}"`

and `"${EXTRACTED#*|}"`

split on the pipe delimiter, a bare `|`

makes both empty strings, which is treated the same as ccusage not being used.

The reason for splitting with parameter expansion instead of using something like `python3 -m json.tool`

is to shave off one subshell. If this gets embedded in a status line, the call frequency could get high, so I stack up small efficiencies.

`isdigit()`

check
The bash→Python bridge goes through `sys.argv`

.

```
RESULT=$(python3 - "$LOG" "${CC_OUTPUT_TOK:-}" "${CC_COST_5H:-}" <<'PY' 2>/dev/null
import sys, json, datetime, collections

log_path, cc_out_str, cc_cost_str = sys.argv[1], sys.argv[2], sys.argv[3]
cc_out = int(cc_out_str) if cc_out_str.isdigit() else None
try:
    cc_cost = float(cc_cost_str) if cc_cost_str else None
except ValueError:
    cc_cost = None
```

`${CC_OUTPUT_TOK:-}`

is the pattern for expanding an undefined variable to an empty string under `set -u`

. In environments where ccusage isn't installed, `CC_OUTPUT_TOK`

stays undefined, so without this the script dies with `unbound variable`

.

`cc_out_str.isdigit()`

rejects empty strings, decimals, negative values, and the string `None`

all in one shot. Passing an empty string to `int()`

raises `ValueError`

, so you'd need try/except — but for an integer check, `isdigit()`

fits in one line. `cc_cost`

is handled with `try/except ValueError`

because ccusage returns decimals like `"0.001234"`

.

Reading the code, cost-log.jsonl gets opened twice. There's a first pass and a second pass.

```
# 1パス目
with open(log_path) as f:
    for line in f:
        ...
        if t >= cutoff_5h:
            pass  # ← 実際には何もしない
        if t >= cutoff_7d:
            day = t.strftime("%Y-%m-%d")
            sess_7d_by_day[day].add(sid)
```

The first pass is now essentially dead code. It builds `sess_7d_by_day`

, but downstream it's the `by_day`

Counter (updated in the second pass) that actually gets used. It's leftover code from the implementation process.

What's effective is the `latest`

dictionary in the second pass (lines 100–124 of the code).

```
latest = {}
with open(log_path) as f:
    for line in f:
        try:
            r = json.loads(line)
            t = datetime.datetime.fromisoformat(r["ts"])
        except Exception:
            continue
        key = (r.get("session_id", ""), r.get("transcript", ""))
        prev = latest.get(key)
        if (prev is None) or (t > prev[0]):
            latest[key] = (t, r)

for (sid, _tr), (t, r) in latest.items():
    out = int(r.get("output", 0))
    cost = float(r.get("cost_usd", 0))
    if t >= cutoff_5h:
        out_5h  += out
        cost_5h += cost
        n_5h    += 1
```

Keyed on `(session_id, transcript)`

, it keeps overwriting whenever a line's timestamp is newer. After the loop ends, iterating `latest.items()`

walks only the final settled value for each session/transcript.

**Why this is necessary**: because Claude Code writes JSONL incrementally during a session. Every time the same transcript in the same session grows "8,000 → 18,400 → 29,700 → 44,100 tokens," a line is appended with the cumulative value at that point. Naively summing all lines gives 8,000+18,400+29,700+44,100 = 100,200, but the correct consumption is the final value, 44,100.

The `result`

dictionary has a `source_diff_pct`

field.

```
diff_pct = None
if cc_out is not None and own_out_5h > 0:
    diff_pct = round(abs(cc_out - own_out_5h) / max(cc_out, own_out_5h) * 100, 1)

result = {
    ...
    "source_diff_pct": diff_pct,
    "ccusage_used": cc_out is not None,
    ...
}
```

It doesn't appear in `--short`

mode, but it's included in the detailed JSON output. It records, as a percentage, the divergence between the ccusage-derived token count and the cost-log.jsonl-derived one.

If this keeps exceeding 20%, that's a sign that one of the data sources is broken or that ccusage's data structure has changed. In normal operation you never see it, but when something feels off about the numbers, running `token-budget-advisor.sh`

manually (no arguments) prints the detailed JSON, and this value tells you which source to suspect.

`_short`

format
The `advice`

field joins everything together when multiple flags are raised (lines 161–171 of the code).

```
advice_parts = []
if s5 == "critical":
    advice_parts.append(f"5h output {out_5h/1000:.0f}k超過: 一旦休憩推奨")
elif s5 == "warn":
    advice_parts.append(f"5h output {out_5h/1000:.0f}k接近: 重い作業は次ブロックへ")
if sw == "warn":
    advice_parts.append(f"7d cost ${cost_7d:.0f}: MAX定額枠の消費過多")
if burst:
    advice_parts.append(f"直近3d平均 {avg_sess:.1f}sess/day: 集中作業中")
if not advice_parts:
    advice_parts.append("budget healthy")
```

When "5h is warn AND weekly is also warn AND burst" overlap, `advice`

lists three items separated by slashes. Grepping the detailed-JSON-mode logs afterward tells you how often those compound states occur.

The `_short`

format rounds to thousands with `{out_5h/1000:.0f}k tok`

(line 196 of the code).

```
"_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",
```

`:.0f`

displays an integer with the decimals truncated. `312000 → 312k`

reads much better. For cost display, the 5h figure has one decimal place and the weekly one is an integer, evening out the visual information density.

The first version had `set -eo pipefail`

in it.

One morning I opened `~/.claude/dashboard.md`

and the contents were empty. The mtime was from that morning, but the file size was 0 bytes.

launchd jobs only have `/usr/bin:/bin:/usr/sbin:/sbin`

on PATH. ccusage, installed via nvm, lives at `~/.nvm/versions/node/v24.13.0/bin/ccusage`

, which isn't on the path in a launchd environment. `ccusage blocks --json`

returned exit 127 with `command not found`

, and under `-e`

the script died instantly.

`dashboard.sh`

's command substitution `$( token-budget-advisor.sh --short )`

propagated that exit code, the whole redirect block `{...} > "$OUT"`

was cancelled, and OUT became 0 bytes.

The fix came in two steps.

```
# 修正前
set -eo pipefail
...
CC_JSON=$(ccusage blocks --json)  # ccusage がなければ exit 127 → 即死

# 修正後
set -u  # -e を外す
...
CC_JSON=$(ccusage blocks --json 2>/dev/null || true)  # 失敗しても exit 0、CC_JSON は空文字
```

Ending `fail_open()`

with `exit 0`

is the design I derived from this experience. There are still days when the single line `budget: ⚫ n/a`

shows up on the dashboard, but that's meaningful information — "there was a day ccusage couldn't be read" — and it's far easier to debug than a blank page.

The first implementation didn't use the `latest`

dictionary; it just summed every line.

```
# 危険な初期実装
with open(log_path) as f:
    for line in f:
        r = json.loads(line)
        t = datetime.datetime.fromisoformat(r["ts"])
        if t >= cutoff_5h:
            out_5h += int(r.get("output", 0))  # 全行合算
```

One night, after a long stretch of heavy work, the `--short`

output showed `🔴 cap-near (5h:2541k tok...)`

. The threshold is 1.2M, so 2.5M is physically impossible. It exceeds the MAX plan's ceiling.

Opening cost-log.jsonl directly, there were 30-plus lines with the same `session_id`

reading `"output": 11200`

, `"output": 23800`

, `"output": 39500`

, and so on. I'd been adding up every cumulative value Claude Code writes incrementally during a session.

After fixing it to group by `(session_id, transcript)`

and take only the last line, the same session read `🟡 burst (5h:843k tok...)`

. That was the correct number.

This experience confirmed that **the output field in cost-log.jsonl is a cumulative value, not a delta**. You can't guess that from the filename — it's a bug that only surfaces once you run it against real data.

I'd forgotten to add `ensure_ascii=False`

to the Python output.

```
# 危険な初期実装
print(json.dumps(result))  # ensure_ascii=False なし
```

Here's the kind of string that came out of `--short`

mode.

```
🟢 OK (5h:312k tok $1.2 / 7d:$48)
```

The 🟢 (U+1F7E2) had become a surrogate-pair escape. Print that to a terminal and, depending on how zsh handles the string, you either get `\ud83d`

displayed literally as characters, or the prompt-width calculation goes off and the cursor position breaks.

```
# 修正後
print(json.dumps(result, ensure_ascii=False))
```

Python 3's default is `ensure_ascii=True`

(escaping non-ASCII characters as `\uXXXX`

). Japanese advice strings break the same way. `ensure_ascii=False`

is a mandatory specification for JSON serialization that handles emoji or Japanese.

`ccusage blocks --json`

comes back with a structure like this.

```
{
  "blocks": [
    { "isActive": true, "tokenCounts": { "outputTokens": 412000 }, "costUSD": 1.52 },
    { "isActive": false, "tokenCounts": { "outputTokens": 980000 }, "costUSD": 3.61 },
    { "isActive": false, "tokenCounts": { "outputTokens": 542000 }, "costUSD": 2.01 }
  ]
}
```

At first I wasn't filtering on `isActive`

and was summing `outputTokens`

across all blocks.

```
# 危険な初期実装
d = json.load(sys.stdin)
out = sum(b.get("tokenCounts", {}).get("outputTokens", 0) for b in d.get("blocks", []))
# → 412000 + 980000 + 542000 = 1,934,000 になる
```

It added in past blocks too, so it always came out critical.

The fix pulls out only the active block (lines 41–47 of the code).

```
active = [b for b in d.get('blocks', []) if b.get('isActive')]
if active:
    b = active[0]
    tc = b.get('tokenCounts', {}) or {}
    out = int(tc.get('outputTokens', 0))
    cost = float(b.get('costUSD', 0))
    print(f'{out}|{cost}')
else:
    print('|')
```

The doubled `{}`

in `tc = b.get('tokenCounts', {}) or {}`

is also worth a look. When `tokenCounts`

comes back as `null`

(right after a block starts, for instance), `get()`

returns `None`

. `None or {}`

becomes `{}`

, so the following `.get("outputTokens", 0)`

doesn't crash. A `get()`

default alone can't prevent the `null`

→ `None`

case, so `or {}`

is necessary.

`set -u`

I originally wrote the `--short`

mode check like this.

```
# 危険な初期実装
if [ "$1" = "--short" ]; then
  MODE="--short"
fi
```

`set -u`

exits 1 immediately when an undefined variable is referenced. Calling `token-budget-advisor.sh`

with no arguments produced the error `$1: unbound variable`

and died.

```
# 修正後
MODE="${1:-json}"
```

`${1:-json}`

uses `json`

as the default value when `$1`

is undefined or empty. A no-argument call becomes `MODE=json`

and passing `--short`

becomes `MODE=--short`

, which coexists with `set -u`

.

Since I also aligned the subsequent checks to `[ "$MODE" = "--short" ]`

, every reference to `$1`

disappeared from the script. Small defenses like this are bugs you don't notice until "it suddenly dies in production cron."

Most of these stumbles only surfaced by "building a version that runs first, then running it against real files." Even if you verify the threshold logic with unit tests, you can't catch the cost-log.jsonl double-counting problem until you feed it a real file. The launchd PATH problem doesn't reproduce until you register it with cron and run it for the first time.

There are bugs you can only see with real data and a real environment. The structure that keeps you from leaving those to "it should work" — fail-open, `⚫ n/a`

in `--short`

, the `source_diff_pct`

debug info — stacked up, and now the daily dashboard runs without ever going blank.

To build "a mechanism that notices before the environment falls apart," you first crush every place you personally got stuck. That's the unglamorous core of maintaining a ¥1.2M/month autonomous setup.

The previous chapter went through five stumbles in detail with real code (blank dashboard, the 2.5M-token anomaly, broken emoji, no `isActive`

filter, the `set -u`

no-argument crash). Here I'll list the additional gotchas I actually hit, in bullet form. These are mostly ones that surfaced after going into operation.

**Forgetting the single quotes on the heredoc EOF**

The main aggregation section embeds the Python script in bash with a `<<'PY'`

heredoc. At first I wrote `<<PY`

(no quotes). Do that and bash expands variables inside the document. For instance, even if you've written `log_path = sys.argv[1]`

in your Python code, the moment a `$HOME`

appears inside the heredoc, bash replaces it with the home directory path. The script works syntactically, but you discover the problem — a hardcoded path — when you run it on a different machine. The single quotes in `<<'PY'`

disable bash's variable expansion completely. The rule in this script is to unify bash→Python value passing on `sys.argv`

alone, so there's no need whatsoever for variables inside the heredoc.

**How the first pass became dead code**

Reading the actual script, cost-log.jsonl gets opened twice (the first pass on lines 80–96, and the second pass on lines 100–111). Inside the first pass is this comment.

```
if t >= cutoff_5h:
    # transcript 同一の場合は最新行で上書き集計したい → ここは単純合算で OK
    # (cost-log は session ごとに累積値で書かれているので、最新行のみ採用すべき)
    pass
```

It's `pass`

. It does nothing. I initially tried to do "take only the last line" in a single pass, but you can't know "whether this line is the last one" until you read the next line. To keep overwriting during a scan, "last" isn't settled until you've read the whole file. So a second pass became necessary, and the first pass was left with only the code that aggregates session counts into `sess_7d_by_day`

. But what ultimately gets used is the `by_day`

Counter updated in the second pass, and `sess_7d_by_day`

isn't used either. The evolution of the implementation is left in the code as a fossil.

**A timezone naive/aware collision silently skips every line**

If cost-log.jsonl's `ts`

field is in a timezone-bearing format like `2026-08-02T05:12:33+00:00`

, `datetime.datetime.fromisoformat(r["ts"])`

returns a tz-aware `datetime`

. Meanwhile, the aggregation reference time is computed like this.

```
now = datetime.datetime.now()
cutoff_5h  = now - datetime.timedelta(hours=5)
```

`datetime.now()`

is tz-naive. In the `t >= cutoff_5h`

comparison, naive and aware collide, and on Python below 3.11 you get `TypeError: can't compare offset-naive and offset-aware datetimes`

. But because this code sits inside `try/except Exception: continue`

, the exception never reaches the console and the line is simply skipped. If every line is skipped, `out_5h=0`

stays as-is and processing finishes with a normal value (zero tokens) rather than `cost-log.jsonl not found`

. The output becomes `🟢 OK (5h:0k tok $0.0 / 7d:$0)`

— the hardest bug to notice, appearing as the phenomenon "for some reason the cost is zero."

**An assumption about launchd job names broke**

Line 38 of `dashboard.sh`

has this code.

```
launchctl list | grep com.shun | awk '{printf "- %s exit=%s\n", $3, $2}' | head -15
```

It's a grep that assumes launchd jobs are created with the `com.shun.*`

naming convention. Jobs created with a different convention don't show up at all. There were days when the dashboard's "Scheduled Jobs" section showed only one entry, and I misread it as "the jobs are gone." In reality the grep pattern just didn't match the job names. Since launchctl's listing puts the canonical job name in the `Label`

column, you need to change the pattern to match your own environment's job naming convention.

**Single quotes collide inside python3 -c**

The ccusage parsing section (lines 37–52 of the script) uses the form `printf '%s' "$CC_JSON" | python3 -c "..."`

. The reason you can use Python single quotes inside `"..."`

is that the outer quoting is double quotes.

``` python
EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "
import sys, json
d = json.load(sys.stdin)
active = [b for b in d.get('blocks', []) if b.get('isActive')]
...
" 2>/dev/null || echo "|")
```

The single quotes in `d.get('blocks', [])`

don't terminate the bash string. That's because I chose the approach of passing JSON via stdin. Had I written `-c 'import sys...'`

directly, the internal Python single quotes would terminate the bash string and cause a syntax error. I choose between `printf ... | python3 -c "..."`

and `python3 - <<'PY' ... PY`

based on whether the script is short or long.

**Status-line integration cost 500ms on every Enter**

At first I put the command substitution directly in zsh's `PROMPT`

.

```
PROMPT='%F{blue}%~%f $(~/.claude/scripts/token-budget-advisor.sh --short) %# '
```

The script runs every time you press Enter. Python startup (about 80ms) + reading cost-log.jsonl (50–200ms depending on line count) + the ccusage call (200–400ms) stacked up, and in sessions with heavy work the wait exceeded a perceptible 500ms. The solution is to switch to letting `dashboard.sh`

handle it. `dashboard.sh`

runs daily via cron and updates `~/.claude/dashboard.md`

(line 104 of dashboard.sh does `cat "$OUT"`

). Putting a one-line command in the status line that reads that cache is much lighter. Alternatively, you can put it in tmux's `status-right`

with a 30-second update interval.

**I only noticed once source_diff_pct went past 20%**

It doesn't appear in the normal `--short`

output, but running with no arguments prints a value like `"source_diff_pct": 23.4`

in the detailed JSON.

```
diff_pct = round(abs(cc_out - own_out_5h) / max(cc_out, own_out_5h) * 100, 1)
```

It's the divergence rate between the ccusage-derived and cost-log.jsonl-derived token counts. One day, quality felt degraded even though I shouldn't have been over the threshold. Running manually with no arguments, `source_diff_pct`

was 28.1. The cause was that ccusage's data structure had changed subtly in the previous update — a key name inside `tokenCounts`

had changed. If `source_diff_pct`

is near zero (within 5%), the two sources are consistent. Since I built a habit of checking it manually on a regular basis, I've been able to catch numeric drift early.

I've now walked through the implementation and all the stumbles. Here are 15 practical rules I wished I'd known from the start after actually running this.

**1. Write monitoring scripts fail-open**

The worst pattern is the monitoring dying and taking the main thing with it. `fail_open()`

finishes with `exit 0`

and, in `--short`

mode, prints `⚫ n/a`

. Since it's used in the command substitution on line 79 of `dashboard.sh`

, if the advisor dies the budget line becomes `⚫ n/a`

. A record saying "the day we couldn't fetch it" is easier to debug than a blank page. The point of fail-open isn't to swallow errors — it's to leave the state "fetch failed" behind as text.

**2. Use only set -u and drop -e**

`set -u`

turns undefined variables into immediate errors and catches typos early. But adding `-e`

means a failing external command terminates the entire script. For cron integration scripts, no `-e`

is the right answer. `dashboard.sh`

also uses `set -uo pipefail`

(line 3), while advisor.sh uses only `set -u`

(line 15). Even if the caller has `-uo pipefail`

, as long as the callee returns `exit 0`

, the whole command-substitution block survives.

**3. Keep two data sources so it works when either is missing**

The design lets it aggregate from cost-log.jsonl alone even in environments without ccusage. It doesn't break on a dev machine, a production machine, or a PATH-restricted launchd environment. When ccusage is available, it takes priority (lines 127–131 of the script). With a single-source dependency, the script dies every time the installation state changes.

**4. Take only the last line per (session_id, transcript) key**

The `output`

in cost-log.jsonl is a cumulative value, not a delta. Every time the same session grows `11200 → 23800 → 39500 → 44100`

, a line is appended. Summing all lines gives 118,600, but the correct value is 44,100. Building the `latest`

dictionary across two passes (lines 100–111) and using only the final settled values in the aggregation produces the correct number. Miss this and you'll always be in critical.

**5. Fit integer validation in one line with isdigit()**

Passing values between bash and Python lets empty strings, non-numerics, and None slip in.

```
cc_out = int(cc_out_str) if cc_out_str.isdigit() else None
```

`isdigit()`

rejects empty strings, decimals, negative values, and the string `None`

. Passing an empty string to `int()`

raises `ValueError`

, so you'd need try/except — but for an integer check, `isdigit()`

fits in a single line. Decimals (ccusage's `costUSD`

is a string like `"1.524"`

) are handled with `float()`

+ try/except.

**6. Always pair a get() default with or {}**

`b.get('tokenCounts', {})`

returns an empty dict if the key is absent, but returns `None`

if the key exists and the value is `null`

. A `get()`

default alone can't prevent `null`

→ `None`

.

```
tc = b.get('tokenCounts', {}) or {}
```

Adding `or {}`

converts `None`

into an empty dict too. It prevents the following `.get('outputTokens', 0)`

from throwing `AttributeError`

in cases where `tokenCounts`

comes back as `null`

, such as right after a block starts.

**7. Never forget ensure_ascii=False**

Python 3's default is `ensure_ascii=True`

. Both the 🟢 emoji (U+1F7E2) and Japanese advice strings become `\uXXXX`

escapes. Cursor position shifts in terminal output, and a mystery string like `🟢`

lines up in the status line. For JSON serialization containing emoji or Japanese, `json.dumps(result, ensure_ascii=False)`

is a mandatory specification (line 199 of the script).

**8. Make the ccusage call triple-layered**

```
if command -v ccusage >/dev/null 2>&1; then
  CC_JSON=$(ccusage blocks --json 2>/dev/null || true)
  if [ -n "$CC_JSON" ]; then
    EXTRACTED=$(printf '%s' "$CC_JSON" | python3 -c "..." 2>/dev/null || echo "|")
```

Three layers: existence check (`command -v`

) → error suppression (`2>/dev/null || true`

) → parse-failure fallback (`|| echo "|"`

). If PATH in a launchd environment is only `/usr/bin:/bin:/usr/sbin:/sbin`

and ccusage isn't visible, layer 1 skips it. If ccusage exists but the JSON is malformed, layer 2 catches it; if the Python parse fails, layer 3 does.

**9. Pull out only active blocks with an isActive filter**

`ccusage blocks --json`

returns an array that includes past blocks. Summing one active block (410k tokens) + two past blocks (1.52M tokens total) gives 1.93M, which always comes out critical.

```
active = [b for b in d.get('blocks', []) if b.get('isActive')]
```

Narrow down to active blocks before extracting. Process only when an active block exists via `if active:`

, and return the fallback with `print('|')`

when it doesn't.

**10. Consolidate --short mode and the detailed output in one script**

Even though the format differs between status-line embedding and manual checking, splitting the script gives you two maintenance surfaces. When you change a threshold (800k / 1.2M / $3,000 / 5 sess/day), you update only one side and consistency breaks. Use `MODE="${1:-json}"`

to make no-argument default to JSON, and in `--short`

pull out only the `_short`

field (lines 207–208). Because both outputs pass through the same decision engine, numeric consistency is guaranteed.

**11. Continuously record data consistency with source_diff_pct**

It doesn't appear in `--short`

, but the detailed JSON output contains a value like `"source_diff_pct": 4.2`

. It's the divergence rate between the ccusage-derived and cost-log.jsonl-derived token counts. Normally it stays within 5%. If it keeps exceeding 20%, that's a sign that ccusage's data structure changed or cost-log.jsonl's write format changed. When something feels off about the numbers, first run `token-budget-advisor.sh`

manually (no arguments) and check this value.

**12. Use a single-quoted EOF for heredocs: <<'EOF'**

With `<<PY`

, bash expands variables inside the heredoc. Unify bash→Python value passing on `sys.argv`

and eliminate any need for bash variables inside the heredoc. With `<<'PY'`

, expansion is completely disabled and Python's literal strings arrive intact.

**13. Use ${VAR:-} to turn undefined variables into empty strings under set -u**

In environments without ccusage, `CC_OUTPUT_TOK`

stays undefined. Referencing `"$CC_OUTPUT_TOK"`

under `set -u`

dies with `unbound variable`

. `${CC_OUTPUT_TOK:-}`

turns both undefined and empty into "empty string." An empty string reaches `sys.argv[2]`

on the Python side, `cc_out_str.isdigit()`

returns `False`

, and `cc_out = None`

. Rather than swallowing an error, it propagates the state "there is no data" in a type-safe way.

**14. Route status-line embedding through a cache**

Putting a command substitution directly into zsh's `PROMPT`

means it runs on every Enter. Python startup at 80ms + the file read + the ccusage call at 200–400ms stack up into a wait exceeding 500ms during heavy work sessions. Take advantage of the design where `dashboard.sh`

runs daily via cron and updates `~/.claude/dashboard.md`

(line 104 of dashboard.sh), and keep the status line to a one-line command that reads the cache file. Putting it in tmux's `status-right`

with a 30-second update interval also works.

**15. Decide thresholds only after observing 1–2 weeks of real data**

The numbers `THRESH_5H_WARN = 800_000`

/ `THRESH_5H_CRIT = 1_200_000`

weren't fixed from the start. For one to two weeks I manually checked the detailed JSON output of `token-budget-advisor.sh`

(no arguments), confirmed that output density perceptibly thins past 800k tokens, and only then adopted them as thresholds. The optimal values change with your own work patterns. If you're mostly asking light questions, the 5-hour block often resets naturally before you enter warn. The $3,000 weekly cost ceiling is also a number tuned to how I use the MAX plan's flat rate. The order matters: run it first, observe, then decide the numbers.

`token-budget-advisor.sh`

is a little over 200 lines of shell script plus inline Python, but packed into it is nearly every design decision needed to "keep a monitoring system running stably."

The first version I built didn't work. `set -eo pipefail`

blanked the dashboard every morning, summing all lines produced a physically impossible 2.5M-token figure, and the emoji turned into escape strings.

The current implementation is the result of fixing those one by one. `fail_open()`

came from the blank-dashboard experience. The `latest`

dictionary came from the 2.5M-token anomaly. `ensure_ascii=False`

came from the broken emoji. The triple-layered ccusage call came from the always-critical verdict caused by having no isActive filter. `${VAR:-}`

came from the no-argument crash. Every defense corresponds to a bug I actually hit.

What matters in this kind of script is less "the design while it's working" and more "the behavior when it breaks." If the monitoring system goes down, you can't notice quality degradation in what it monitors. If a single `⚫ n/a`

line comes out when it breaks, it remains as information: "we couldn't get data today." That's completely different from a blank page.

Just adding one line to line 79 of `dashboard.sh`

means yesterday's fuel-consumption summary gets written into the morning dashboard automatically.

```
echo "  budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
```

The cognitive cost of checking token headroom mid-work went to zero. Instead of quality degrading without my noticing and me realizing the next morning that "yesterday's code looks sketchy," the decision to push heavy work into the next 5-hour block comes naturally.

A ¥1.2M/month autonomous setup runs not on flashy AI features but on an accumulation of unglamorous instruments like this one.

I've put the whole picture of the setup, the breakdown of the ¥1.2M, and a 30-day procedure into a paid note.

📕 [Claude Code自律環境で、実際どう稼ぐか ― 仕組み・実例・始め方・サポート](https://note.com/bokuwalily/n/n849b3a07784a)

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