Metadata-Version: 2.4
Name: claude-session-budget
Version: 1.1.3
Summary: Track Claude Code's 5-hour session usage from local JSONL and pause tool calls before hitting the limit. No API calls, no network.
Home-page: https://github.com/Star001-KR/claude-session-budget
Author: Taehyung Kim
License: MIT License
        
        Copyright (c) 2026 Taehyung Kim
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/Star001-KR/claude-session-budget
Project-URL: Repository, https://github.com/Star001-KR/claude-session-budget
Project-URL: Issues, https://github.com/Star001-KR/claude-session-budget/issues
Keywords: claude,claude-code,session,budget,rate-limit,hook,pretooluse
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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

```mermaid
%%{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/<project-path>/<session-id>.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:

```bash
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](hooks/hooks.json),
the [skill](skills/budget-check/SKILL.md) becomes available as
`/session-budget:budget-check`, and all scripts run from `${CLAUDE_PLUGIN_ROOT}/scripts/`.

### Option B — Homebrew

```bash
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`:

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

### Option C — PyPI

```bash
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`):

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

For PM-layer / orchestrator integration:

```python
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)

```bash
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`:

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

### Option E — Claude Code Skill (manual copy)

```bash
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)

```python
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:

```python
{
  "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):

```bash
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:

```bash
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`:

```bash
BUDGET_PAUSE_MODE=block
```

You can opt into sleep mode:

```bash
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
