DEV Community

Lily
Lily

Posted on • Originally published at dev.to

Stop Before You Hit the 5-Hour Block Cap: A Token Sentinel Built from ccusage and a Cost Log

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:

  1. Get the current active block's output-token count in real time
  2. Distinguish warn (heavy consumption) from critical (almost out) with numbers
  3. 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 (集中作業の疑い)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

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

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
Enter fullscreen mode Exit fullscreen mode

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.sh side that calls token-budget-advisor.sh does not wrap it in run_to. The advisor itself is fail-open and returns exit 0 by design, so hangs are unlikely. That said, there remains a corner case where python3 gets stuck, so I plan to wrap it in run_to 10 in 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 方針
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 the latest = {} 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>&1 branch does graceful degradation, and the fallback jsonl aggregation takes over
  • autopilot misread ⚫ n/a from --short as "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 burst to misfire. Fixed to dedupe with a set in sess_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_pct self-verifies accuracy
  • --short mode 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)