cd /news/developer-tools/catch-the-5-hour-cap-before-you-hit-… · home topics developer-tools article
[ARTICLE · art-63483] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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

A developer created token-budget-advisor.sh, a tool that warns users before hitting Claude MAX's 5-hour output token cap by detecting dangerous thresholds around 800k tokens and critical levels past 1.2M tokens. The script integrates into a dashboard and status line, reading from Claude's stdin and ccusage blocks to provide absolute token counts rather than percentages, preventing abrupt session halts during unattended background jobs.

read5 min views1 publishedJul 17, 2026

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

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.

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

doubles as its design.

#

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}')
    ...")

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

.

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)

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

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)

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.

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"

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

--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})",

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)

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.

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

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.

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>

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.

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

Note

Label matching alone can let something slip through right at the boundary. For critical decisions, it's safer to pull the5h_output_tokens

field out of the JSON output (without--short

) as a number and check>= 1200000

directly.

(session_id, transcript)

keycommand -v ccusage

and keep a fallback path that runs on cost-log.jsonl alone when it's missing⚫ n/a

output to protect the dashboard's other sectionsProgramArguments

through /bin/zsh -c

so it picks up zsh's pathTHRESH_SESS_PER_DAY

depending on your usagerate_limits

is --

before the first turntoken-budget-advisor.sh

aggregates from a source_diff_pct

(session_id, transcript)

key, cost-log gives you a --short

mode compresses down to a single 🟢/🟡/🔴

line, usable directly in shell gates and dashboard embeddingdashboard.sh

auto-updates on launchd's StartInterval 14400 (4h), keeping the budget line always currentNext 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*

── more in #developer-tools 4 stories · sorted by recency
── more on @claude max 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/catch-the-5-hour-cap…] indexed:0 read:5min 2026-07-17 ·