In my last post, "Auto-pruning dormant plugins," I wrote about automating plugin cleanup. This time I'm going one level upstream: a mechanism that stops you before you burn through your token budget in the first place.
Claude Code's 5-hour blocks have an output-token ceiling, and once you cross it you're rate-limited until the next block begins. The problem is that the design only lets you notice after you've gone over. In an environment where autopilot and nightly batches run in the background, I actually had this happen: I woke up to find every slot already SKIP-ped. token-budget-advisor.sh is the script I wrote to "stop before the cap."
The problem: the limit only became visible after I'd blown past it
When multiple launchd automation jobs run across a 5-hour block, you can't tell which job consumed how much. The output of claude -p doesn't print token counts, and the Claude Code UI is only visible during a session. Because there was no way to check remaining budget, the cap was always an after-the-fact notification.
I needed three things:
- Get the current active block's output-token count in real time
- Distinguish warn (heavy consumption) from critical (almost out) with numbers
- Emit it continuously in a form that fits on one line in the dashboard and daily-brief
The 5-hour block thresholds
The comment at the top of the script records the thresholds I settled on based on real measurements.
# しきい値:
# 5h block: output > 800k → warn (>1.2M で critical)
# weekly: cost > $3000 → warn
# sessions: >5/day → warn (集中作業の疑い)
The reason 800k is the warn line is that once you cross it, running a heavy model for a while can push you to critical in one or two turns. 1.2M is the decision line for "move any heavy work to the next block." These two stages keep the damage small if you ignore the warning and keep running.
ccusage × cost-log.jsonl: a dual-source design
Relying on a single source for the token count is dangerous. An environment where ccusage isn't on the PATH, the first turn where blocks is empty, a missed jsonl write — if any of these is missing and the sentinel stops, the entire guard collapses. That's why I use a dual-source setup that treats ccusage as authoritative and cost-log.jsonl as a fallback.
# 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('|')
except Exception:
print('|')
" 2>/dev/null || echo "|")
CC_OUTPUT_TOK="${EXTRACTED%|*}"
CC_COST_5H="${EXTRACTED#*|}"
fi
fi
ccusage blocks --json returns a blocks[] array, and the block with isActive: true is the current 5-hour window. It pulls out tokenCounts.outputTokens and uses it for the decision.
On the Python aggregation side, if the ccusage value is valid it takes precedence, and the difference from cost-log.jsonl is recorded as source_diff_pct.
# 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
# 比較 (検証用)
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)
A large source_diff_pct (5% or more) is a sign that the cost-log.jsonl aggregation logic has drifted. The structure lets the sentinel verify its own accuracy.
The decision and --short output
The decision logic has three statuses.
THRESH_5H_WARN = 800_000 # output tokens
THRESH_5H_CRIT = 1_200_000
THRESH_WEEK_WARN = 3000 # USD
if out_5h >= THRESH_5H_CRIT:
s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
s5 = "warn"
else:
s5 = "ok"
On top of that, it detects "an average of more than 5 sessions/day over the last 3 days" with a burst flag. A pattern where short bursts of intensive work drain the budget is a precursor to weekly cost climbing, so it raises a warning on a separate axis.
Here's the output of --short mode.
🟢 OK (5h:234k tok $1.2 / 7d:$45)
🟡 burst (5h:821k tok $4.1 / 7d:$89)
🔴 cap-near (5h:1234k tok $6.7 / 7d:$122)
In the code it's stored under the _short key.
"_short": f"{icon} {label} (5h:{out_5h/1000:.0f}k tok ${cost_5h:.1f} / 7d:${cost_7d:.0f})",
Integration into the dashboard and daily-brief
The Cost section of dashboard.sh looks like this.
echo "## 💰 Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo " budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
cost-summary.sh --short returns a cumulative cost like $45.20 / 31 sess (7d), and right below it the 5-hour block's current position is lined up as budget:. The point is that every time you open the dashboard, you see both cost and remaining budget.
daily-brief.sh calls cost-summary.sh 7 through a timeout wrapper called run_to 60, and slots 10 lines into the brief's cost section.
echo "## 💰 cost(直近 7d)"
run_to 60 ~/.claude/scripts/cost-summary.sh 7 2>&1 | head -10
run_to is a wrapper around gtimeout --kill-after=15 <seconds>, designed so that even if an aggregation script hangs, it won't kill the entire brief.
Note: The
dashboard.shside that callstoken-budget-advisor.shdoes not wrap it inrun_to. The advisor itself is fail-open and returnsexit 0by design, so hangs are unlikely. That said, there remains a corner case where python3 gets stuck, so I plan to wrap it inrun_to 10in the future.
Why the fail-open design
The top of the script has set -u but -e is deliberately removed.
set -u # -e は外す: fail-open 方針
The fail-open helper looks like this.
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"
If a script embedded in the dashboard returns exit 1, the dashboard-generation script itself stops via the -o pipefail chain. For the sentinel, "emit ⚫ n/a and let it pass through" is safer than "the sentinel itself dies and stops the dashboard." Operationally, it's better to see ⚫ n/a and realize "I'll check by hand today" than to lose the means of checking cost entirely.
Installing ccusage
npm install -g ccusage
# または
npx ccusage blocks --json
ccusage blocks returns usage per 5-hour block as JSON. Note that without --json it produces decorated terminal-oriented output. When no block with isActive: true exists (right after a block expires), an empty array is returned and the script automatically falls back.
Pitfalls I hit
-
Running without ccusage double-counts sessions in cost-log → To adopt only the latest line, I added logic that re-fetches the final line using
(session_id, transcript)as the key (the second pass over thelatest = {}loop in the script) -
I looked only at the label (🟡) and let it pass even when remaining budget was negative → I added a separate numeric check on the autopilot side (
REMAINING=$((800000 - BLOCK_OUT))stops it at<= 0) -
ccusage is managed by nvm and wasn't on launchd's PATH → The
command -v ccusage >/dev/null 2>&1branch does graceful degradation, and the fallback jsonl aggregation takes over -
autopilot misread
⚫ n/afrom--shortas "no problem" → I changed the autopilot-side check to an OR of "contains 🔴 or critical," and added⚫as a skip target as well -
The by_day aggregation didn't deduplicate by session_id → Sessions with multiple lines on the same day inflated the per-day count and caused
burstto misfire. Fixed to dedupe with a set insess_7d_by_day
Summary
- The 5-hour block output-token thresholds are warn: 800k / critical: 1.2M
- The source is a dual-source of ccusage first + cost-log.jsonl fallback.
source_diff_pctself-verifies accuracy -
--shortmode returns a one-line 🟢/🟡/🔴 summary and slots into the dashboard and daily-brief - Fail-open (
exit 0+⚫ n/a) is a design where the sentinel doesn't kill the dashboard - The final guard isn't just the label decision — it's recomputing remaining budget numerically and stopping
Next time, I plan to write about the mechanism where autopilot automatically switches to a lighter model when it's about to cross these thresholds — the coordination between variable effort and model routing.
Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*
Top comments (0)