Skip to main content

Track Claude Code's 5-hour session usage from local JSONL and pause tool calls before hitting the limit. No API calls, no network.

Project description

claude-session-budget

Track Claude Code's 5-hour session usage locally — and automatically pause task queues before hitting the limit.

Discovered by reverse-engineering ~/.claude/projects/**/*.jsonl
No API calls. No web scraping. Pure local file parsing.

The Problem

Claude Code enforces a rolling 5-hour session limit. When running automated task queues or background agents, the session can hit its limit mid-task with no warning.

How It Works

%%{init: {'themeVariables': {'fontSize': '13px'}}}%%
flowchart TD
    A([You run a task in Claude Code]) --> B[Claude Code logs API response<br/>to local JSONL]
    B --> C[budget_check.py hook<br/>fires before next tool call]
    C --> D[find_session_anchor:<br/>scan for bridge_status ts]
    D -->|anchor found| E1[cutoff = anchor ts<br/>only post-anchor msgs count]
    D -->|no anchor| E2[cutoff = now − 5h<br/>plain rolling window]
    E1 --> F[Sum weighted tokens after cutoff]
    E2 --> F
    F --> G{Usage % vs<br/>calibrated limit}
    G -->|&lt; 80%| H([✓ Proceed silently])
    G -->|80–93%| I([⟳ Re-sync + log estimate])
    G -->|≥ 93%| J[⏸ Block dispatch until<br/>5-hour session resets]
    J -.->|wait for reset| A

Claude Code writes every API response to local JSONL files: ~/.claude/projects//.jsonl

Each assistant message contains token counts in a usage field. By summing these with pricing-ratio weights and calibrating against one /usage observation, we estimate session usage in real time.

Session Anchor (bridge_status)

A pure 5-hour rolling window over-counts when older sessions linger in jsonl. Claude Code records a type=system, subtype=bridge_status line whenever /remote-control activates — a strong signal that a new active session has begun.

find_session_anchor() looks for the most recent bridge_status ts inside the rolling 5h window. When found, the scan cutoff is raised to that ts and only newer messages count toward the budget. The next reset estimate becomes anchor + 5h, which lines up with Anthropic's /usage reset time within minutes.

When no anchor is present (idle gaps, tool restarts), the logic falls back to the plain 5h rolling window — same behavior as before. The anchor is intermittent by design; the fallback keeps the tool useful even when the signal is stale.

Token Weighting (Opus pricing, input = 1.0)

Token Type Weight
input_tokens 1.00×
cache_creation_input_tokens 1.25×
cache_read_input_tokens 0.10×
output_tokens 5.00×

Calibration

The calibrated limit is auto-learned from real Anthropic API errors:

  1. Every time budget_check.py runs, it inspects each in-window jsonl entry for the structural API-error signature: type=system, subtype=api_error with HTTP status=429, or any nested error.type containing rate_limit / usage_limit.
  2. When it finds a new event, it takes the weighted token total at that moment as a real-world 100% reading.
  3. The stored limit is EWMA-merged with the observation (default α=0.3) and written to ~/.claude/.budget_calibration.json.

Why structural matching, not text? An earlier version regex-matched "rate limit" / "limit reached" in the raw jsonl line. That picked up any user/assistant message body that mentioned the topic — including conversations debugging this very tool — and produced a self-poisoning EWMA loop that drove the calibrated limit from 63M down to 16M, causing false 100% BLOCKING. Structural signature matching eliminates that class of false positive.

You can also seed/refine the limit manually with one /usage reading:

python3 scripts/calibrate.py --observed-pct 67

Known baselines (used until auto-learning kicks in):

  • Claude Max (5x): ~63,226,913 weighted tokens = 100% (measured 2026-05-09)
  • Claude Pro: unknown — contributions welcome

Installation

Option A — Claude Code Plugin Marketplace (Recommended)

This repo is itself a Claude Code marketplace. Inside Claude Code:

/plugin marketplace add Star001-KR/claude-session-budget
/plugin install session-budget

The PreToolUse hook is wired automatically via hooks/hooks.json, the skill becomes available as /session-budget:budget-check, and all scripts run from ${CLAUDE_PLUGIN_ROOT}/scripts/.

Option B — Homebrew

brew tap Star001-KR/claude-session-budget https://github.com/Star001-KR/claude-session-budget
brew install Star001-KR/claude-session-budget/claude-session-budget

This installs the budget-check and budget-calibrate commands into $(brew --prefix)/bin. To wire budget-check as a Claude Code PreToolUse hook, add to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [{"type": "command", "command": "/opt/homebrew/bin/budget-check"}]
      }
    ]
  }
}

Option C — PyPI

pip install claude-session-budget

This installs the budget-check and budget-calibrate console scripts and makes the package importable as claude_session_budget. Wire budget-check as a Claude Code PreToolUse hook the same way as the brew option (the binary will be on your $PATH):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [{"type": "command", "command": "budget-check"}]
      }
    ]
  }
}

For PM-layer / orchestrator integration:

from claude_session_budget.session_budget_manager import SessionBudgetManager

budget = SessionBudgetManager()
status = budget.get_status()

Option D — Manual Hook (no plugin, no brew, no pip)

curl -fsSL https://raw.githubusercontent.com/Star001-KR/claude-session-budget/main/install.sh | bash

Or manually add to ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/budget_check.py"}]
      }
    ]
  }
}

Option E — Claude Code Skill (manual copy)

mkdir -p .claude/skills/session-budget
cp skills/budget-check/SKILL.md .claude/skills/session-budget/SKILL.md
cp scripts/budget_check.py .claude/skills/session-budget/check.py

Option F — PM Layer / Orchestrator (manual)

import sys; sys.path.insert(0, "scripts")  # or install as a package
from session_budget_manager import SessionBudgetManager

budget = SessionBudgetManager()

async def dispatch_task(task):
    wait_secs = await budget.check_before_dispatch()
    if wait_secs:
        await asyncio.sleep(wait_secs)
    # Optional: log current state for dashboards / observability
    s = budget.get_status()
    log.info(f"{s['pct']}% — resets in {s['remaining_str']} (epoch={s['reset_at']})")

get_status() returns a dict with both raw numbers and a human-friendly remaining string:

{
  "pct": 13.2,
  "weighted_tokens": 2_128_235,
  "calibrated_limit": 63_226_913,
  "reset_at": 1778355198.018,        # epoch seconds (anchor + 5h, or oldest msg + 5h)
  "remaining_secs": 16_755,
  "remaining_str": "4h 39m",         # or "already reset" when remaining == 0
}

Thresholds

Threshold Default Behavior
Sync 80% Re-reads JSONL and logs updated estimate
Pause 93% Blocks by default; optional hook sleep mode can wait and re-check

Set thresholds via env vars or a .env file (loaded automatically):

BUDGET_SYNC_PCT=80 BUDGET_PAUSE_PCT=93 python3 scripts/budget_check.py

.env lookup order — first hit wins per key, but process env always overrides:

  1. ./.env (current working directory — per-project override)
  2. ~/.claude/.env (global default for all sessions)
  3. Built-in defaults

Copy .env.example to get started:

cp .env.example ~/.claude/.env

Pause Modes

By default, budget_check.py blocks immediately when usage reaches the pause threshold. Leave BUDGET_PAUSE_MODE unset, empty, or set to block:

BUDGET_PAUSE_MODE=block

You can opt into sleep mode:

BUDGET_PAUSE_MODE=sleep
BUDGET_RECHECK_SECS=60
BUDGET_RESET_GRACE_SECS=60
BUDGET_MAX_SLEEP_SECS=14400

In sleep mode, the PreToolUse hook process stays alive, periodically re-checks local JSONL usage, and exits 0 once usage falls below the pause threshold. This lets the original tool call continue after the 5-hour window has rolled forward enough. Before resuming, it sleeps BUDGET_RESET_GRACE_SECS and checks one more time.

Important Risks

Sleep mode is experimental and disabled by default.

  • The hook process may remain alive for minutes or hours.
  • Claude Code, your shell, terminal, OS, or task runner may impose timeouts.
  • The UI can appear stuck while the hook is sleeping.
  • If the reset estimate is wrong, the hook may still block after waiting.
  • Sleep mode is best for supervised local use, not unattended automation.

For reliable queue pause/resume behavior, prefer SessionBudgetManager in an orchestrator or PM layer.

Environment Variables

All variables can be set in process env, ./.env, or ~/.claude/.env. First match wins per key, but process env always overrides.

Variable Default Description
BUDGET_SYNC_PCT 80 Sync threshold (% of limit). At/above this, hook logs an estimate update
BUDGET_PAUSE_PCT 93 Pause threshold (% of limit). At/above this, hook blocks (or sleeps)
BUDGET_PAUSE_MODE block block → exit 2 immediately. sleep → keep hook alive, re-check periodically
BUDGET_RECHECK_SECS 60 sleep mode: jsonl re-scan interval
BUDGET_RESET_GRACE_SECS 60 sleep mode: extra wait after threshold drop, before resume
BUDGET_MAX_SLEEP_SECS 14400 sleep mode cap (4h). After this, hook gives up and exits 2
BUDGET_EWMA_ALPHA 0.3 EWMA smoothing factor for auto-learned limit
BUDGET_CALIBRATED_LIMIT (unset) Hard override of stored calibrated limit (weighted tokens)
BUDGET_PROJECTS_DIR ~/.claude/projects jsonl scan root
BUDGET_CALIBRATION_FILE ~/.claude/.budget_calibration.json Persistence path for auto-calibration

Limitations

  • Token weights are a proxy — Anthropic's internal formula is not public
  • Peak hours (weekday 5–11am PT) consume limits faster
  • Cross-device usage is not tracked (JSONL files are local only)
  • The bridge_status anchor is intermittent: it appears when /remote-control activates, not on every tool call. When stale (long idle gaps) the tool falls back to the plain 5h rolling window
  • The rate-limit api_error signature is conservative — accepts both status=429 and any inner error.type containing rate_limit/usage_limit. We haven't directly observed a real 429 jsonl line yet, so the exact inner type string can be tightened once one shows up
  • Recalibrate after plan changes

Files

Path Description
.claude-plugin/plugin.json Plugin manifest (name, version, author, license)
.claude-plugin/marketplace.json Marketplace manifest — lets /plugin marketplace add resolve this repo
hooks/hooks.json PreToolUse hook declaration using ${CLAUDE_PLUGIN_ROOT}
skills/budget-check/SKILL.md Claude Code skill definition (auto-discovered as /session-budget:budget-check)
scripts/budget_check.py Lightweight hook script (no deps); also runs auto-calibration
scripts/session_budget_manager.py Full async class for PM/orchestrator integration
scripts/calibrate.py Manual calibration entry from a /usage reading
scripts/_budget_core.py Shared core: .env loader, JSONL scan, anchor detection, signature matcher, EWMA learner
tests/test_budget_core.py Unit tests (44) — env loading, jsonl scan, anchor, signature matcher, EWMA
.env.example Copy to ./.env or ~/.claude/.env
install.sh One-line installer for the manual (non-plugin) hook setup
Formula/claude-session-budget.rb Homebrew formula (used when this repo is added as a brew tap)
pyproject.toml PyPI packaging metadata (PEP 517/621) — pip install claude-session-budget
scripts/__init__.py Marks scripts/ as the claude_session_budget package via package-dir mapping
docs/internals.md Architecture deep-dive (anchor + 5h fallback + signature matcher + EWMA)
LICENSE MIT

Contributing

PRs welcome — especially calibration values for Pro and Max 20x plans.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

claude_session_budget-1.1.1.tar.gz (20.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

claude_session_budget-1.1.1-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file claude_session_budget-1.1.1.tar.gz.

File metadata

  • Download URL: claude_session_budget-1.1.1.tar.gz
  • Upload date:
  • Size: 20.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for claude_session_budget-1.1.1.tar.gz
Algorithm Hash digest
SHA256 456fd11dfd167bc887f93246fe1aed04e8034f8ae1e55c689b0ca104a65ed833
MD5 0aa7d953c28630082225c4abcecb68a5
BLAKE2b-256 be20ad41821e2fcaaa21d53b3f03bb0b861af97c6bebe631b254fb13def8ea19

See more details on using hashes here.

Provenance

The following attestation bundles were made for claude_session_budget-1.1.1.tar.gz:

Publisher: publish.yml on Star001-KR/claude-session-budget

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file claude_session_budget-1.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for claude_session_budget-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5602a81bc9326ba549a8ab1790260f055274d7c3104a340cf81c24a510654abf
MD5 2bbf75c12840dc31c4973fecc85dbed0
BLAKE2b-256 32f5606e8aa43c60f665c2ab0f81777d446459997482831829895bb0e1620ac2

See more details on using hashes here.

Provenance

The following attestation bundles were made for claude_session_budget-1.1.1-py3-none-any.whl:

Publisher: publish.yml on Star001-KR/claude-session-budget

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page