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 -->|< 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:
- Every time
budget_check.pyruns, it inspects each in-window jsonl entry for the structural API-error signature:type=system, subtype=api_errorwith HTTPstatus=429, or any nestederror.typecontainingrate_limit/usage_limit. - When it finds a new event, it takes the weighted token total at that
moment as a real-world
100%reading. - 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:
./.env(current working directory — per-project override)~/.claude/.env(global default for all sessions)- 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_statusanchor is intermittent: it appears when/remote-controlactivates, not on every tool call. When stale (long idle gaps) the tool falls back to the plain 5h rolling window - The rate-limit
api_errorsignature is conservative — accepts bothstatus=429and any innererror.typecontainingrate_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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
456fd11dfd167bc887f93246fe1aed04e8034f8ae1e55c689b0ca104a65ed833
|
|
| MD5 |
0aa7d953c28630082225c4abcecb68a5
|
|
| BLAKE2b-256 |
be20ad41821e2fcaaa21d53b3f03bb0b861af97c6bebe631b254fb13def8ea19
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_session_budget-1.1.1.tar.gz -
Subject digest:
456fd11dfd167bc887f93246fe1aed04e8034f8ae1e55c689b0ca104a65ed833 - Sigstore transparency entry: 1488406995
- Sigstore integration time:
-
Permalink:
Star001-KR/claude-session-budget@f54c1b56964a29c573e10ecf6901e9a60bfb6f45 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Star001-KR
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f54c1b56964a29c573e10ecf6901e9a60bfb6f45 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file claude_session_budget-1.1.1-py3-none-any.whl.
File metadata
- Download URL: claude_session_budget-1.1.1-py3-none-any.whl
- Upload date:
- Size: 16.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5602a81bc9326ba549a8ab1790260f055274d7c3104a340cf81c24a510654abf
|
|
| MD5 |
2bbf75c12840dc31c4973fecc85dbed0
|
|
| BLAKE2b-256 |
32f5606e8aa43c60f665c2ab0f81777d446459997482831829895bb0e1620ac2
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_session_budget-1.1.1-py3-none-any.whl -
Subject digest:
5602a81bc9326ba549a8ab1790260f055274d7c3104a340cf81c24a510654abf - Sigstore transparency entry: 1488407043
- Sigstore integration time:
-
Permalink:
Star001-KR/claude-session-budget@f54c1b56964a29c573e10ecf6901e9a60bfb6f45 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Star001-KR
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f54c1b56964a29c573e10ecf6901e9a60bfb6f45 -
Trigger Event:
workflow_dispatch
-
Statement type: