antelier-stop
v0.2.0
Published
Local spend budgets, Claude Code tool-call brakes, and token receipts.
Maintainers
Readme
antelier-stop
A local dollar-budget brake for Claude Code sessions and subagent fan-out. The status line senses spend; a PreToolUse hook refuses the next tool call at the configured ceiling; a JSON receipt records token costs, subagents, and tool-call counts. No account, server, runtime dependencies, bundler, or build step. Requires Node 20 or newer.
Use
Run from the project whose Claude Code settings you want to configure:
npx antelier-stop init --session 4 --fanout 2 --per-subagent 0.75
# Add --strict to make hook errors block too.
npx antelier-stop show
npx antelier-stop raise 2
npx antelier-stop offBefore npm publication, invoke the source directly: node /absolute/path/to/antelier-stop/cli.js init --session 4. This source package has not been published by this verification.
init copies this package's runtime files into .antelier/stop-runtime/<version>/ (staged, then renamed into place; never overwritten) and points every hook at that copy, so the hooks keep working after npm prunes the npx cache or a global install moves (0.1.1; 0.1.0 pointed at the cache). off removes the copies. init writes to .claude/settings.local.json, the personal, git-ignored settings file, because the commands carry this machine's absolute Node path; the shareable .claude/settings.json is never touched (a status line found there is chained). In a git repository init adds .antelier/ and .claude/settings.local.json to .gitignore once; off leaves .gitignore alone. Hooks are installed in exec form (command + args, no shell) for PreToolUse (all tools, five-second timeout), SubagentStart, SubagentStop, and SessionEnd, so spaces, $ and backticks in a project path cannot be re-parsed, on Git Bash or PowerShell. The status line is a shell command quoted for sh / Git Bash; a Windows machine without Git Bash loses the sensor, not the brake. Re-run init from the newer package to upgrade the vendored runtime (show says when hooks run an older version than the command you typed). Turning the brake off while a session is running removes the hooks first and the runtime second; a hook already starting in that instant fails open, which is what off means. Existing command status lines run first with the original stdin, and their output precedes our segment. Previous-command failures are reported on stderr; the spend segment still appears.
off removes our marked hooks and restores the previous status line. An unchanged install/off round trip restores the original settings bytes, including LF or CRLF. If settings were edited meanwhile, removal preserves unrelated text and entries. .antelier/stop-settings.json stores the original settings and previous status command for restoration; keep it until uninstalling. Receipts and configuration are retained. Treat .antelier/ as private local data; init adds .antelier/ and .claude/settings.local.json to .gitignore once in a git repository and never touches ignore files otherwise.
status and hook read Claude Code JSON from stdin. status prints, for example, $1.42 / $4.00 · 5h 31% · 7d 12%; missing limits or budgets are omitted. It writes .antelier/spend/<session_id>.json. The brake uses a sensor no older than 30 seconds; otherwise it sums the main transcript and separate subagent transcripts. Claude Code flushes the assistant message carrying a turn's usage at about the moment it dispatches PreToolUse, so without a fresh sensor the hook waits, up to transcript_wait_ms (default 1500) in .antelier/stop.json, for the transcript to grow before measuring; every tool call follows a new assistant message, so no growth means not flushed yet (a live claude -p session on 2026-09-06 measured a $0.32 first turn as $0 without this). claude -p has no status line, so it always takes this path. claude --safe-mode runs no settings-file hooks at all: the brake is off there. Receipt model breakdowns always come from transcripts and may lag a fresh sensor total. Unreadable breakdowns cannot override an already observed over-budget sensor.
raise 2 adds $2 to the current session ceiling, preserving the sensor's original freshness timestamp. It selects CLAUDE_SESSION_ID if set, otherwise the last session observed in this project. For multiple sessions, use raise 2 --session-id <id> explicitly. Raises survive subsequent status updates. This changes the session ceiling only; change fanout or per_subagent in the config to increase those caps.
Budgets and prices
init stores budgets in .antelier/stop.json. Amounts must be finite and nonnegative; zero means refuse immediately at that ceiling. raise requires a positive amount. An optional override looks like this (all rates are USD per million tokens):
{
"session": 4,
"fanout": 2,
"per_subagent": 0.75,
"strict": false,
"prices": {
"claude-sonnet-5": {
"input": 3,
"output": 15,
"cache_read": 0.3,
"cache_write": 3.75
}
}
}Defaults: list prices as of 2026-09-05, override in .antelier/stop.json.
| Model | Input | Output | Cache read | Cache write | |---|---:|---:|---:|---:| | claude-fable-5-1 | 15 | 75 | 1.5 | 18.75 | | claude-opus-5 | 15 | 75 | 1.5 | 18.75 | | claude-sonnet-5 | 3 | 15 | 0.3 | 3.75 | | claude-haiku-4-5-20251001 | 1 | 5 | 0.1 | 1.25 |
Unknown models price at $0 and appear as unpriced: <model> in receipts and summaries. Overrides replace all four rates for a model and are named in the receipt. A fresh sensor uses Claude Code's reported total, not our override. Thinking tokens are already part of output tokens and are not counted again. Cache creation uses its total when present, otherwise the sum of its one-hour and five-minute token counts, at the configured cache-write rate. Repeated snapshots with the same assistant message ID are counted once using their latest usage.
Per-subagent spend sums the matching agent-<agent_id>.jsonl or unique agent-<name>-<agent_id>.jsonl file under <dirname(transcript_path)>/<session_id>/subagents/. If no match exists, it uses the newest file created after the recorded SubagentStart, marked estimated. No suitable file means attribution is explicitly unavailable, with no invented cost. fanout caps aggregate subagent transcript spend. If live agents times the per-subagent floor exceeds the remaining session or fanout budget, SubagentStart records a warning; the new agent's next tool call is refused while that reservation still exceeds the budget. It does not try to deny the non-blocking start event.
Receipts and limits
A refusal and SessionEnd write .antelier/receipts/<ISO-timestamp>-<unique-id>.json. Colons are replaced with hyphens for Windows filenames. The summary names the budget and spend, agents whose tool calls were refused, available last progress, the receipt, and the raise command. show repeats the latest receipt summary. Receipts contain model token/cost totals and pricing provenance, per-agent attribution, first-observed/end times, agents running at refusal, and PreToolUse attempt counts by tool. There is no supported dollar attribution per tool. Progress comes from the first 200 characters of SubagentStop.last_assistant_message; it is unavailable until that event arrives.
Default refusal is deny JSON with exit 0. Default hook errors log to stderr and fail open. --strict installs hook commands that exit 2 for refusal or a caught error, including malformed stdin. Node or shell launch failure and an externally killed/timed-out hook remain outside the JavaScript error handler. A bounded local lock serializes concurrent state updates; errors acquiring it follow the same fail-open/strict policy. A crashed process can leave .antelier/stop.lock; after verifying no hook is using it, remove that empty directory to recover.
This is a brake on the next tool call, not a billing-system guarantee: transcript/status updates can lag, token-only work and an already running call can overshoot, unknown models are unpriced, and unavailable attribution cannot enforce an exact tree cap. No running process is killed. Parsed usage is cached by file size, mtime, and ctime within a process; separate hook processes do not share that cache.
It does not bill, phone home, read code beyond the transcript's token counts, know the plan's real quota (it prices tokens; rate-limit percentages come from the status line when present), undo the last call, or replace judgment. It examines local transcript usage/model/message identifiers and hook metadata, not project source files. Receipts also retain the requested short subagent progress text. A chained command is an existing user-configured program with its own behavior.
Captured refusal, live
Verified 2026-09-05 on Windows with Node 22: a temporary repository, init --session 0.002, then a real
Claude Code session (claude -p) asked to run five shell commands. The PreToolUse hook refused the first
Bash call. The hook's stdout, reproduced against the same transcript:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"antelier-stop: session budget $0.0020 reached ($0.32); receipt .antelier/receipts/2026-09-05T19-05-43.111Z-5a5c768a-497d-4978-af04-815fc00c95e0.json; raise with: npx antelier-stop raise 2.00"}}What Claude then told the user, verbatim: "The first command was blocked before it ran. The antelier-stop
hook reports the session budget is set to $0.00 and already at $0.23, so it refuses every shell call. Nothing
executed. Receipt was written to .antelier/receipts/…. Unblocking requires raising the budget, which is a
spending decision I won't make for you." (The "$0.00" in that quote was this package's own display bug for
sub-cent budgets, fixed in the same commit; budgets under a cent now print with four decimals.)
The receipt for that session: total_cost_usd 0.318 summed from the transcript (claude-fable-5-1, list
prices as of 2026-09-05), by_tool {"Bash": 1} (attempt counts only; tokens cannot be attributed to tools),
no subagents. Two runs of this test cost about $0.30 in total.
Captured fan-out, live
Same day, a second temporary repository: init --session 5 --fanout 0.05 --per-subagent 0.02, then a real
Claude Code session asked to launch three subagents in parallel, each to cat one file. Every subagent's
first tool call was refused, and the main agent quoted each reason back rather than retrying:
antelier-stop: per-subagent budget $0.02 reached ($0.40); receipt .antelier/receipts/2026-09-05T19-13-52.744Z-….json; raise with: npx antelier-stop raise 2.00
antelier-stop: fan-out budget $0.05 reached ($0.40); receipt .antelier/receipts/2026-09-05T19-13-54.886Z-….json; raise with: npx antelier-stop raise 2.00
antelier-stop: per-subagent budget $0.02 reached ($0.18); receipt .antelier/receipts/2026-09-05T19-13-57.293Z-….json; raise with: npx antelier-stop raise 2.00The session-end receipt: total_cost_usd 1.67 from the transcript; by_subagent three rows, each
"attribution": "exact: subagent transcript" at $0.53, $0.30 and $0.30; stopped_subagents lists all
three agent ids; by_tool {"Agent": 3, "Bash": 3}. The first turn of a subagent already costs more than
two cents, which is why a $0.02 per-tree cap refuses at the first call; that is the point of the test, not a
recommended setting.
Still not verified: the five-minute walkthrough from npx antelier-stop init by a naive user, blocked until the
package is on npm.
Verified live, 0.1.1 (2026-09-06)
Fresh git repository, the packed 0.1.1 tarball, npx antelier-stop init --session 0.002, then a real claude -p session (Claude Code 2.1.263, Windows, Git Bash present) asked to run cat f.txt. The exec-form hook installed in .claude/settings.local.json and running from .antelier/stop-runtime/0.1.1/ refused the first shell call; Claude's own reply, verbatim:
The command did not run. A hook blocked it and returned this refusal, verbatim:
antelier-stop: session budget $0.0020 reached ($0.28); receipt .antelier/receipts/2026-09-06T17-24-40.845Z-….json; raise with: npx antelier-stop raise 2.00
No output from f.txt was produced.The run before the transcript wait existed (same setup, same budget) let the same call through and measured it at $0.32 only at session end; that miss is why transcript_wait_ms exists. The same session under claude --safe-mode ran no settings-file hooks at all (Claude Code debug log: "Found 0 total hooks in registry"), so the brake is off in safe mode.
