Real-time USD cost enforcement for Claude Code. Halts runaway agentic loops mid-step before they burn through your budget.
cost-guard is a Claude Code plugin. Once installed, it runs automatically on every session and every prompt:
- Mid-step halt (via the
PostToolBatchhook) stops the agentic loop between model iterations if the current step's cost, the session's total cost, or the burn rate crosses its configured limit. This keeps one greedy step from cascading into a $30+ spend in minutes. - Subagent fan-out cap (via
PreToolUse) denies additionalAgent/Taskspawns inside a single step once the per-step spawn count is reached. - Prompt gate (via
UserPromptSubmit) blocks the next user prompt if the session has already exceeded its cap, rate, or step limit. - Compaction gate (via
PreCompact) blocks context compaction when the budget is already crossed (compaction is an expensive hidden model call).
Rates are per model with effective-date ranges, not per tier — Opus 4.1 and Opus 5 are 3x
apart, so a tier lookup priced Opus 5 sessions at Opus 4.1 rates and halted roughly 3.7x too
early. cost-guard refresh-pricing re-derives the table from the published pricing page and is
the only command that touches the network; status --json reports how stale the table is.
All enforcement runs from hooks reading the JSONL transcript Claude Code writes for every session. All thresholds are in USD. No network calls. Python 3 standard library only.
A bottom-row indicator is available as an opt-in extra — /cost-guard:install-indicator — that shows live cost metrics at the bottom of the Claude Code UI. It is purely cosmetic; halts run identically whether the indicator is installed or not. If you already have a bottom-row command configured (from /statusline or another plugin), the installer detects it and asks whether to replace it (cost-guard takes over the slot, three reliable rows; your previous command is backed up and restored by uninstall-indicator) or compose with it (cost-guard runs your existing command and appends its own three rows below, both visible).
⚠ Compose mode is fragile and not recommended for multi-line statuslines. Claude Code's bottom-row render area has a finite height and width; multi-line output (e.g. a starship statusline emitting 1–2 rows + cost-guard's 3 rows) frequently truncates with an ellipsis on the first visible line. The Claude Code statusline docs explicitly warn that "multi-line status lines are more prone to rendering issues than single-line plain text." Prefer
replacemode for a stable, predictable indicator. Usecomposeonly if your existing statusline is a single short line that leaves room for cost-guard's three rows underneath.
ℹ The plugin sandbox prevents cost-guard from writing to
~/.claude/settings.jsondirectly. When you run/cost-guard:install-indicator(or:uninstall-indicator), Claude Code's plugin sandbox denies the write. The plugin detects this and prints the exact JSON snippet you need to paste into~/.claude/settings.jsonby hand; once you save the edit and restart Claude Code, the indicator appears. The plugin's own state (indicator.jsonand the persistent backup) is written automatically — only the settings.json side requires the manual paste.
The indicator renders three lines when enough data is available:
Cost Guard — token-cost watchdog
💵 session: $1.23 (cap $10.00) 🔥 rate: $0.24/min (limit $0.50) 👣 step: $0.45 (cap $2.00)
🧠 ctx: 42% 🎰 slot: $5.25 of ≈$15.00 plan (35%)
Header changes when state changes:
- Active — pink
Cost Guard — token-cost watchdog - Paused — yellow
Cost Guard (paused) — halts disabled for this session - Halted — red
Cost Guard (HALTED) — <human-readable reason>(e.g.burn rate over limit,session cap reached,step exceeded cap,subagent fan-out cap reached). The badge clears on the next non-bypass user prompt — once you submit normal work, cost-guard takes that as acknowledgment that the cause was resolved. - Disabled globally — dim
Cost Guard (off) — runaway protection disabled
Columns on line 2 and line 3 align: the second column on line 3 (slot) starts at the same screen position as the second column on line 2 (rate). Cell colors follow the threshold logic — green under the warn level, amber between warn and cap, red at/over.
| Field | Meaning |
|---|---|
💵 session: $X (cap $Y) |
Current session USD spend vs the per-session cap. |
🔥 rate: $X/min (limit $Y) |
Trailing 5-minute USD burn rate vs the per-minute limit. |
👣 step: $X (cap $Y) |
USD spent in the current step vs the per-step hard cap. Resets on every new UserPromptSubmit. |
🧠 ctx: N% |
Percent of the Claude Code context window currently in use (from Claude Code's own data, always available). |
🎰 slot: $X of ≈$Y plan (N%) |
For Claude.ai Pro/Max subscribers. The estimated USD value your 5-hour rate-limit slot represents, and how many of those dollars have already been consumed across all sessions in the current window. Derived from the trailing-5h cost-ledger total (across every Claude Code session under ~/.claude/projects/) divided by the slot percentage Claude Code reports — i.e. the actual $_used ÷ slot_pct × 100, not a single-session extrapolation. Lets you map the USD caps onto your subscription's slot in actual-dollar terms: if plan ≈ $15 and you've used $5.25 (35%), you have about $9.75 of slot left before Anthropic throttles. Shown only once the slot meter has moved past 0.5% and the trailing-5h cost is at least $0.05; before that, the cell collapses to 🎰 slot: N% (calibrating). See Known limitations for the cases where this estimate is biased. |
max_usd_per_session(default$10) — hard cap on total spend across every step in this Claude Code session. Once cumulative session spend crosses this,UserPromptSubmitblocks new prompts andPostToolBatchhalts any in-progress step.max_usd_per_step(default$2) — hard cap on what one single step can spend. A "step" is one prompt → Claude responds → tool batch → Claude responds → tool batch → ... until Claude stops.PostToolBatchhalts the loop between tool batches as soon as the step's cost crosses this. Primary defense against a single greedy step.rate_per_min_usd(default$0.50) — trailing 5-minute session burn-rate cap.max_subagent_spawns_per_step(default8) — count-based cap on subagent fan-out per step.
Why two caps? They defend against different failure modes:
- A long session that slowly accumulates cost across many normal steps →
max_usd_per_sessioncatches it. - A greedy single step (e.g., Claude fans out 20 subagents or runs 50 tool calls in one response cycle) →
max_usd_per_stepcatches it mid-step, before the session cap has any reason to fire.
Example with defaults ($10 session, $2 per-step): you can have five normal $1.50 steps (total $7.50, neither cap tripped). But if one step starts burning through $3, cost-guard halts it at ~$2 before it can complete — without waiting for the session total to hit $10.
Session cost comes from the JSONL transcript (transcript_path is provided to every hook) multiplied by the shipped pricing table. Everything is derived from data the hook already has — nothing is read from any UI surface.
/plugin marketplace add anthropics/claude-plugins-community
/plugin install cost-guard@claude-community
Distributed via anthropics/claude-plugins-community — Anthropic's curated marketplace for community-contributed Claude Code plugins. Every plugin listed there has been submitted via claude.ai, passed automated security scanning, and been approved for distribution. Halts activate on the next SessionStart.
This repository is the plugin (cost-guard's source lives at the root):
git clone git@github.com:oggeh-dev/claude-cost-guard.git
claude --plugin-dir claude-cost-guardThis repo is the canonical source. The community marketplace tracks tagged releases (currently pinned to a specific commit SHA) and serves as the primary install path; clone-and---plugin-dir is for local development against main.
| Command | What it does |
|---|---|
/cost-guard:status |
Prints current session spend, rate, and configured caps. |
/cost-guard:diag |
Full plugin-state dump: file sizes, env vars, recent halts, effective config, current bottom-row command. Use this when something looks wrong. |
/cost-guard:set-session-limit <USD> |
Sets the per-session USD cap. |
/cost-guard:set-step-limit <USD> |
Sets the per-step USD cap. |
/cost-guard:set-rate <USD/min> |
Sets the per-minute burn-rate limit. |
/cost-guard:set-subagent-cap <N> |
Sets the max subagent spawns per step. |
/cost-guard:set-warn <USD> |
Sets the amber-warn threshold on session spend. |
/cost-guard:pause |
Globally disables halts (warning: runaway protection is off). |
/cost-guard:resume |
Re-enables halts. |
/cost-guard:install-indicator [compose|replace] |
Install the bottom-row indicator. With no argument, if a bottom-row command is already configured, cost-guard shows the choice and exits without modifying anything. |
/cost-guard:uninstall-indicator |
Remove cost-guard's indicator and restore whatever was there before (compose / replace backups are byte-exact restores). |
The shipped binary is also on PATH while the plugin is enabled, so you can run it directly from any shell:
cost-guard status
cost-guard set-session-limit 30
cost-guard status --jsonAll knobs are declared in plugin.json as userConfig fields. Claude Code prompts for each value when the plugin is enabled. You can override any field at runtime via /cost-guard:set-* commands (stored in ${CLAUDE_PLUGIN_DATA}/overrides.json).
| Field | Default | Description |
|---|---|---|
enabled |
true | Master switch. When false, no halts fire. |
max_usd_per_session |
10.00 | Hard cap on total session spend. UserPromptSubmit and PostToolBatch halt when the session ledger crosses this. |
max_usd_per_step |
2.00 | Hard cap per single Claude step. PostToolBatch halts when one step crosses this. Primary defense against one greedy step. |
rate_per_min_usd |
0.50 | Trailing 5-minute burn-rate cap. Session-filtered. |
max_subagent_spawns_per_step |
8 | PreToolUse denies further Agent/Task tool calls within a step once this count is reached. |
warn_at_usd |
7.00 | Amber warning on UserPromptSubmit when session spend crosses this (below cap). |
seed_from_history |
true | On first install, scan recent ~/.claude/projects transcripts to populate the rolling ledger. Gives accurate trailing burn-rate on prompt #1 after install. |
log_decisions |
true | Write every halt/warn to ${CLAUDE_PLUGIN_DATA}/halt-log.jsonl. |
All cost data comes from the JSONL transcripts Claude Code writes for every session. The pricing table ships in pricing.json and is verified against Anthropic's public rates. No network calls.
Claude Code exposes hooks that fire at specific lifecycle events. Two of them can stop an agentic loop mid-run:
PostToolBatch— fires after every batch of tool calls, before the next model invocation. Exit code 2 here stops the loop. This is the primary defense against a $4/min runaway: once the step cost, session cost, or session burn rate crosses the limit, the plugin stops the loop before the next model call can compound the spend.PreToolUsewith matcherAgent|Task— fires before each tool call. Exit code 2 denies the specific call. Used to cap subagent fan-out within a single step.
Other supporting hooks:
UserPromptSubmitgates the next prompt when the session cap, rate limit, or step cap is already blown.PreCompactblocks compaction when the budget is tight (compaction is an expensive hidden model call).SessionStartseeds the ledger on first install.
Every hook invocation is the same command: ${CLAUDE_PLUGIN_ROOT}/bin/cost-guard halt-check. The binary dispatches internally based on the hook_event_name field.
| Code | Meaning |
|---|---|
0 |
Allowed. Either the budget is fine, cost-guard is disabled, or the session is paused. |
2 |
Halted. stderr explains the breach and lists the exact commands to raise the limit, pause, or end the session. |
| Other | Never produced. Plugin bugs are caught at the top level and exit 0 so Claude Code is never blocked by a defect in this code. The failure is logged to halt-log.jsonl. |
All persistent state lives in ${CLAUDE_PLUGIN_DATA} which Claude Code resolves to ~/.claude/plugins/data/<plugin-id>/ for marketplace installs or a local equivalent for --plugin-dir development:
| File | Purpose |
|---|---|
cost-ledger.jsonl |
Append-only rolling 5-hour cost log. Compacted on every SessionStart. |
overrides.json |
Runtime overrides set by /cost-guard:set-* commands. |
sessions/<session-id>.json |
Per-session state: step start, subagent spawn counter, paused flag, last-ingested transcript timestamp. |
halt-log.jsonl |
Audit log of every halt, warn, and error decision. |
.seeded |
Marker file indicating seed_from_history has run once. Delete to re-seed. |
indicator.json |
Present only if you installed the bottom-row indicator. Records which mode (solo/compose/replace), the wrapped command (compose mode), and the exact statusLine entry backed up at install time for byte-for-byte restore on uninstall. |
- Reinstall / update: re-run
/plugin install cost-guard@claude-communityor swap the--plugin-dir.${CLAUDE_PLUGIN_DATA}survives. - Re-seed from history: delete
${CLAUDE_PLUGIN_DATA}/.seeded. The nextSessionStartwill rebuild the ledger from~/.claude/projects. - Uninstall:
/plugin uninstall cost-guard@claude-community. Pass--keep-datato preserve the ledger in case you re-install later. - Reset everything: delete
${CLAUDE_PLUGIN_DATA}.
These are intrinsic to the data and platform behavior cost-guard depends on; the indicator's 🎰 slot: cell is the cell most affected.
cost-guard only sees costs recorded in JSONL transcripts under ~/.claude/projects/. Tokens consumed via claude.ai web/desktop or other Claude apps still count against your 5-hour slot but don't appear in the ledger. Users who split work across those surfaces will see the slot: cell underestimate the true slot value — the bigger your non-Claude-Code share, the bigger the gap.
The indicator's data has two sources: shared on-disk files (cost-ledger.jsonl, slot_state.json) that any indicator can update, and the per-session stdin payload Claude Code passes to that session's status-line subprocess on each refresh.
Shared on-disk data — the cost numerator ($X) and cap projection (≈$Y) — stays fresh in idle sessions because indicators running in other active sessions keep the files current; every indicator reads them on each refresh.
The stdin payload is per-session. Its rate_limits.five_hour.used_percentage (the displayed Z%) and resets_at (used to anchor the cost window) are populated by Claude Code from the response headers of the last API call in that specific session. An idle session makes no new API calls, so its payload still carries the slot percentage as it stood the last time that session talked to Anthropic. Active sessions show the live percentage; idle sessions show the percentage frozen at their last activity.
This means in a multi-session workflow, you'll often see two open Claude Code windows that agree on $X and ≈$Y but disagree on Z% — the active one is current, the idle one is stale. The percentage in the idle session self-corrects the moment that session makes any API call (a user prompt, a slash command, anything that triggers a model turn).
This is a Claude Code platform characteristic, not a cost-guard bug. cost-guard cannot synthesize a fresh slot percentage for an idle session because the value comes from server response headers cost-guard never sees directly.
The 5-hour slot resets at discrete points marked by rate_limits.five_hour.resets_at in Claude Code's payload. cost-guard anchors its cost window to the current slot period (resets_at − 5h) so spending from the previous period doesn't pollute the new period's cap projection. Right after a reset there's a short window where the new period has too little data for any meaningful extrapolation; the cell collapses to 🎰 slot: N% (calibrating) until both gates clear: slot ≥ 0.5 % AND ≥ $0.05 of cost-guard-tracked spending in the current period. Both bars are low so the calibrating window typically lasts seconds to a few minutes after a reset, then the dynamic estimate kicks in and $X = ≈$Y × Z/100 always holds. The cap will look small at first and converge upward as the period accumulates data — that's the estimate honestly reflecting the new period's $-per-pp ratio, which depends on your current model mix (Opus / Sonnet / Haiku, cache hit rates, output ratios) and is generally not the same as the previous period's ratio.
Halts fire too often. Raise max_usd_per_session, max_usd_per_step, or rate_per_min_usd. All three are configurable at runtime via the set-* skills.
Halts never fire. Run /cost-guard:status and confirm enabled: true. Check ${CLAUDE_PLUGIN_DATA}/halt-log.jsonl for recorded decisions; if it is empty, the hooks may not be registered — confirm via claude --debug.
Halt fired but I need to ship. Run /cost-guard:pause to disable until you explicitly /cost-guard:resume. Or raise whichever limit tripped (set-session-limit, set-step-limit, or set-rate).
cost-guard makes zero network requests and contains no analytics or telemetry. All data it reads and writes lives on your machine only. See PRIVACY.md for the full statement of what the plugin reads, what it writes, and what control you have over it.
MIT — see LICENSE.
