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<br/>logs % to stderr])
    G -->|80–93%| I([⟳ Proceed + log sync notice])
    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

The installer pins to the tagged release and verifies downloaded scripts against SHA256SUMS from that release before writing to ~/.claude/hooks.

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 — process env always overrides:

  1. ~/.claude/.env (global default, always loaded)
  2. ./.env (current working directory) — opt-in: set BUDGET_LOAD_PROJECT_ENV=1 to enable the per-project override
  3. Built-in defaults

Migration from <1.1.4: ./.env is no longer auto-loaded by default. To restore per-project override behavior, add BUDGET_LOAD_PROJECT_ENV=1 to ~/.claude/.env (or your shell env).

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 or ~/.claude/.env. Process env always overrides. ./.env (cwd) is opt-in via BUDGET_LOAD_PROJECT_ENV.

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
BUDGET_LOAD_PROJECT_ENV (unset) Set to 1 to also load ./.env from cwd at module import. Disabled by default to avoid an untrusted-cwd attack surface and import-time side effects

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 (53) — 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.3.tar.gz (22.8 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.3-py3-none-any.whl (17.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: claude_session_budget-1.1.3.tar.gz
  • Upload date:
  • Size: 22.8 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.3.tar.gz
Algorithm Hash digest
SHA256 843c379ad8246ca7e18889a7322d29c211d0ea9f511327fb335f2ece6b87b4b9
MD5 2e42e4c93bfc260958bcbc3367b3574a
BLAKE2b-256 2ba27f3a5e682db712e836f9315ad2351ce9e78f4c0bf4d7b9d0a8b9d0c3f078

See more details on using hashes here.

Provenance

The following attestation bundles were made for claude_session_budget-1.1.3.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.3-py3-none-any.whl.

File metadata

File hashes

Hashes for claude_session_budget-1.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 1e33057fb94a8db00c875c8a414f1b0b011cdba1a72b160ab6ea00ff7ab92d03
MD5 bdceaa3745ea74d940662bc0695bf97e
BLAKE2b-256 e617134d2dc08e81fba64fc8a08cf5e52d63e9e4c888be1c412640acdfc86115

See more details on using hashes here.

Provenance

The following attestation bundles were made for claude_session_budget-1.1.3-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