DEV Community

Lily
Lily

Posted on • Originally published at dev.to

Catch the 5-Hour Cap Before You Hit It: token-budget-advisor and Status-Line Integration

My last piece, "Auto-resuming a session that stalled on a rate limit," was about what to do after a 5-hour block stops you. This one is about the machinery that lets you notice before it stops you.

Claude MAX's 5-hour block halts your session the instant output tokens hit the ceiling. Even with ๐Ÿ• 5h 82% visible in the status line, you have no idea how many tokens that remaining 18% actually represents. token-budget-advisor.sh detects the boundary in absolute terms โ€” "getting dangerous around 800k, already cooked past 1.2M" โ€” and this article covers its design, its integration into dashboard.sh, and how it gets wired into the status line.

The problem: you can't tell until you hit the cap

The Claude Code status line shows ๐Ÿ• 5h N%. ~/.claude/scripts/statusline.sh reads this straight from Claude's stdin.

H5=$(j '.rate_limits.five_hour.used_percentage // empty')
H5R=$(j '.rate_limits.five_hour.resets_at // empty')
Enter fullscreen mode Exit fullscreen mode

The pcolor function turns the value yellow at 50%+ and red at 80%+, but that's the percentage Claude computed, not an absolute token count. What 80% translates to in tokens shifts depending on your Max plan's contract state, and if you keep going with long completions you hit the cap and get abruptly stopped from the next turn onward.

To check, you have to run ccusage blocks every single time, and if you forget and then kick off a heavy task, you'll cleanly burn through your 5-hour window. When an unattended job is running in the background, everything can slip into an all-SKIP state before a human even notices.

Design of token-budget-advisor.sh

The policy in the header of ~/.claude/scripts/token-budget-advisor.sh doubles as its design.

# ใ—ใใ„ๅ€ค:
#   5h block:  output > 800k  โ†’ warn (>1.2M ใง critical)
#   weekly:    cost  > $3000  โ†’ warn
#   sessions:  >5/day         โ†’ warn (้›†ไธญไฝœๆฅญใฎ็–‘ใ„)
#
# ๅคฑๆ•—ๆ™‚ใฏ exit 0 ใง fail-open (dashboard ็ตฑๅˆใฎใŸใ‚)
# bash 3.2 ไบ’ๆ›
Enter fullscreen mode Exit fullscreen mode

Dual source: ccusage as primary, cost-log.jsonl as secondary

It first tries ccusage blocks --json.

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}')
    ...")
Enter fullscreen mode Exit fullscreen mode

In environments where ccusage isn't present, it runs on cost-log.jsonl alone. When both are available, it prefers ccusage's values and records the discrepancy 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

# ๆฏ”่ผƒ (ๆคœ่จผ็”จ)
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)
Enter fullscreen mode Exit fullscreen mode

When the gap is large, there may be a problem with the cost-log side's aggregation logic, and source_diff_pct serves as a signal for that.

cost-log.jsonl: unless you take "only the latest line," the numbers come out 2โ€“3x too high

cost-log.jsonl gets written across multiple lines under the same (session_id, transcript) key (by design, the cumulative value mid-session is appended). Summing all lines double-counts the tokens.

# cost-log ใฏ session_id ร— transcript ใ”ใจใซ็ดฏ็ฉๅ€คใงๆ›ธใ‹ใ‚Œใ‚‹ไป•ๆง˜ใ€‚
# ๆœ€ๆ–ฐ่กŒใฎใฟๆŽก็”จใ™ใ‚‹ใŸใ‚ใ€(session_id, transcript) ใงๆœ€็ต‚่กŒใ‚’ๅ–ใ‚Š็›ดใ™ใ€‚
latest = {}
with open(log_path) as f:
    for line in f:
        r = json.loads(line)
        key = (r.get("session_id", ""), r.get("transcript", ""))
        prev = latest.get(key)
        if (prev is None) or (t > prev[0]):
            latest[key] = (t, r)
Enter fullscreen mode Exit fullscreen mode

You adopt only the final line, then filter by the 5h/7d windows. Sum every line without knowing this and you'll get numbers 2โ€“3x the real value.

Thresholds and decision logic

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

if out_5h >= THRESH_5H_CRIT:
    s5 = "critical"
elif out_5h >= THRESH_5H_WARN:
    s5 = "warn"
else:
    s5 = "ok"
Enter fullscreen mode Exit fullscreen mode

The intensive-work check is made from "average over the last 3 days > 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
Enter fullscreen mode Exit fullscreen mode

Output format of --short mode

The _short field is generated on the Python side.

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

Example outputs per state (actual token counts will vary).

๐ŸŸข OK (5h:234k tok $0.3 / 7d:$2)
๐ŸŸก burst (5h:841k tok $1.2 / 7d:$8)
๐Ÿ”ด cap-near (5h:1243k tok $2.1 / 7d:$12)
Enter fullscreen mode Exit fullscreen mode

Since you can match just the icon with grep -qE '๐Ÿ”ด|cap-near', you can drop it directly into a shell-script gate. On error it prints โšซ n/a and returns with exit 0 (fail-open design), so the caller doesn't get halted.

Status-line integration and dashboard.sh

Wiring it into the Cost section of dashboard.sh

The ## ๐Ÿ’ฐ Cost (7d) section of ~/.claude/scripts/dashboard.sh looks like this.

echo "## ๐Ÿ’ฐ Cost (7d)"
~/.claude/scripts/cost-summary.sh --short
echo "  budget: $(~/.claude/scripts/token-budget-advisor.sh --short)"
Enter fullscreen mode Exit fullscreen mode

It gets written out to ~/.claude/dashboard.md, so at the start of a project you just open the file to see the budget situation.

Auto-updating every 4 hours with launchd

Here's the config for ~/Library/LaunchAgents/com.shun.dashboard.plist.

<key>Label</key>
<string>com.shun.dashboard</string>
<key>StartInterval</key>
<integer>14400</integer>
Enter fullscreen mode Exit fullscreen mode

14400 seconds = 4 hours. While the Mac is awake, dashboard.sh runs once every 4 hours and the budget line gets rewritten automatically. Without manually running ccusage blocks, you can check the most recent aggregate just by looking at the file.

Using it as a gate for unattended jobs

In unattended scripts like autopilot, you stop a job ahead of time using the --short output (the wiring I introduced in the claude-autopilot article).

BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
if echo "$BUDGET" | grep -qE '๐Ÿ”ด|critical|cap-near'; then
  log "ABORT: budget critical"; exit 0
fi
Enter fullscreen mode Exit fullscreen mode

Note
Label matching alone can let something slip through right at the boundary. For critical decisions, it's safer to pull the 5h_output_tokens field out of the JSON output (without --short) as a number and check >= 1200000 directly.

Pitfalls I hit

  • Summed every line of cost-log.jsonl and the numbers came out 2โ€“3x too high โ†’ switched to a scheme that adopts only the final line per (session_id, transcript) key
  • The script just hung in environments where ccusage isn't on PATH โ†’ check for existence with command -v ccusage and keep a fallback path that runs on cost-log.jsonl alone when it's missing
  • Without fail-open, all of dashboard.sh goes down โ†’ on error, use exit 0 + โšซ n/a output to protect the dashboard's other sections
  • launchd's minimal PATH couldn't find ccusage โ†’ route ProgramArguments through /bin/zsh -c so it picks up zsh's path
  • The burst flag stayed permanently lit during weekend intensive work โ†’ since the check is "last-3-day average > 5 sess/day," raise THRESH_SESS_PER_DAY depending on your usage
  • statusline.sh's rate_limits is -- before the first turn โ†’ because Claude only includes rate_limits in stdin "after a subscription holder's first API response." After the first turn it displays normally.

Summary

  • Detect the 5-hour block's cap in advance by absolute values of 800k / 1.2M, not "after it stops"
  • token-budget-advisor.sh aggregates from a dual source of ccusage (primary) and cost-log.jsonl (secondary), and surfaces the discrepancy via source_diff_pct
  • Unless you adopt only the final line per (session_id, transcript) key, cost-log gives you a 2โ€“3x miscalculation
  • --short mode compresses down to a single ๐ŸŸข/๐ŸŸก/๐Ÿ”ด line, usable directly in shell gates and dashboard embedding
  • dashboard.sh auto-updates on launchd's StartInterval 14400 (4h), keeping the budget line always current
  • Setting it to fail-open on failure (exit 0 / โšซ n/a) prevents the whole dashboard from going down

Next time I'll write about log rotation for the cost-log.jsonl this dashboard references, and compressing old entries before aggregation gets heavy.


Written by **Lily* โ€” I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio ยท X ยท GitHub*

Top comments (0)