@kill-switch/agent-guard
v0.2.0
Published
Kill Switch for coding agents — stop runaway Claude Code / Cursor / Aider sessions from racking up an LLM bill. Native hook + token-metering proxy with per-session and daily-rolling budgets.
Maintainers
Readme
@kill-switch/agent-guard
Kill Switch for coding agents. Stop a runaway Claude Code / Cursor / Aider session from racking up an LLM bill — before it becomes a $4,200 weekend, an $87k month, or a $500M month "after failing to put usage limits on Claude licenses for employees."
A coding agent runs a reasoning loop — read, edit, validate, re-check — and re-sends its entire accumulated context on every tool call. Cost compounds silently. agent-guard puts a hard ceiling on that loop with two complementary surfaces that share one budget:
| Surface | What it is | Stops | Works with |
|---|---|---|---|
| Hook | A Claude Code PreToolUse / UserPromptSubmit / Stop hook that reads the live transcript, prices real token usage, warns at the soft cap and denies the next tool call at the hard cap | A single Claude Code session, gracefully | Claude Code |
| Proxy | A local metering reverse-proxy on the agent's API base URL that counts usage from real responses and returns HTTP 402 at the cap | Any agent — the API literally stops answering | Claude Code, Cursor, Aider, raw scripts |
The hook is the friendly, native stop. The proxy is the dumb hard wall that can't be argued past. Both feed one ledger with two budget scopes:
- Per-session — catches a single runaway run.
- Daily rolling 24h — catches many small sessions quietly adding up.
…each with a soft cap (warn + alert) and a hard cap (block).
Install
npm i -g @kill-switch/agent-guard # provides `agent-guard` and `ksg`Or use it through the main Kill Switch CLI as ks guard … (same engine, one
shared ledger/budget) — see packages/cli.
Developing from this monorepo
The hook is wired into Claude Code by absolute path to dist/cli.js, so it
must be built before install, and the bare agent-guard / ksg commands only
land on your PATH after a link or publish:
# from the repo root
npm run build:agent-guard # compile src → dist (required before install/link)
npm run test:agent-guard # 21 unit tests
# put `agent-guard` / `ksg` on PATH for local dev
cd packages/agent-guard && npm link
agent-guard --help # now resolves
# unlink when done
npm rm -g @kill-switch/agent-guard⚠️ If
agent-guardreportscommand not found(e.g. when a runaway session hits the cap and the recovery command won't run), it just means the package isn't linked/published yet. The block message always prints an absolute-path fallback so recovery works regardless, andks guard …works whenever theksCLI is installed. You can also always pause with zero tooling:touch ~/.kill-switch/agent-guard/PAUSED.
Quick start — Claude Code (hook)
# Wire the hook into ./.claude/settings.json (use --global for ~/.claude)
agent-guard install
# Set your caps (USD)
agent-guard config --session-soft 5 --session-hard 20 --daily-soft 25 --daily-hard 100
# See where you stand any time
agent-guard statusThat's it. On every tool call the hook recomputes the session's real spend from the transcript. Cross the soft cap → Claude sees a warning and you get an alert. Hit the hard cap → the next tool call is denied with a reason, halting the agent.
Quick start — any other agent (proxy)
agent-guard proxy # listens on :8787, meters Anthropic by default
# point your agent at it:
ANTHROPIC_BASE_URL=http://localhost:8787 claude # (if NOT using the hook — see caveat)
OPENAI_BASE_URL=http://localhost:8787/v1 aider # agent-guard proxy --flavor openaiAt the hard cap the proxy returns 402 kill_switch_budget_exceeded instead of forwarding —
the agent can't spend another token.
⚠️ Don't run Claude Code through both the hook and the proxy — they'd each meter the same dollars and double-count. Hook for Claude Code; proxy for everything else.
Budgets
Resolution order (later wins): built-in defaults → ~/.kill-switch/agent-guard/config.json
→ environment variables. Env override lets you tighten a single risky run:
AGENT_GUARD_SESSION_HARD=10 claude # one-off $10 ceiling| Env var | Meaning | Default |
|---|---|---|
| AGENT_GUARD_SESSION_SOFT | per-session warn (USD) | 5 |
| AGENT_GUARD_SESSION_HARD | per-session block (USD) | 20 |
| AGENT_GUARD_DAILY_SOFT | rolling-24h warn (USD) | 25 |
| AGENT_GUARD_DAILY_HARD | rolling-24h block (USD) | 100 |
A cap of 0 disables that check.
Subscription limits (Claude Code Pro / Max)
Dollar caps are the wrong currency for a Pro/Max subscription: you pay a flat fee, so the scarce resource isn't dollars — it's your plan's rate-limit quota, in two rolling windows:
- a 5-hour window (burst protection), and
- a weekly (7-day) window — the real lockout risk, "resets a couple times a month".
Easiest: wire up the statusLine
Claude Code hands its status bar a JSON payload on stdin, and for Pro/Max sessions that payload includes your live standing:
// .claude/settings.json (or settings.local.json)
{ "statusLine": { "type": "command", "command": "agent-guard statusline" } }That's the whole setup. agent-guard statusline reads rate_limits.five_hour and
rate_limits.seven_day off stdin and keeps a live pill in your bar:
🛡 🟢 12%5h · 17%w · 9%d · 5.0wdThis is the primary source, and it's the good kind of boring: it's documented, needs no network call, no credential read, and can't be rate-limited. It also refreshes on every render, so the snapshot the hook paces against is always current.
Two things it can't do, both by design:
rate_limitsonly appears for Claude.ai Pro/Max subscribers, and only after the first API response in a session — so the very first render of a session falls back to the endpoint.- It carries no per-model breakdown. If you have a per-model weekly (e.g. a model-specific cap sitting at 56% while your all-models weekly is at 31%), that is your real constraint and stdin won't show it — see below.
Filling the gaps: agent-guard usage
For the per-model weekly, and when stdin isn't available, agent-guard falls back to Anthropic's
/api/oauth/usage endpoint:
agent-guard usage # → 5-hour, weekly, and per-model (Sonnet/Opus) weekly utilization + resetsIt reads your Claude Code OAuth token from the OS credential store (macOS Keychain
Claude Code-credentials, or ~/.claude/.credentials.json on Linux — used only as a Bearer
header, never logged or stored). When statusLine stdin is already supplying 5h + weekly,
this runs at most hourly, purely to top up the per-model extras.
⚠️ The endpoint is undocumented and genuinely unreliable — it rate-limits aggressively (
429with an hour-longretry-after) and returns401once the stored OAuth token expires. agent-guard now honours the server's cooldown (and backs off exponentially, 5m→1h, on other failures) instead of retrying into the wall; an earlier build hammered it every 120s and kept itself locked out. Treat per-model numbers as best-effort: if the endpoint is unavailable, you'll still get 5h + weekly from stdin, and the guard says so rather than guessing.
macOS Keychain: the first
agent-guard usage/statusmay pop a Keychain prompt — click Always Allow so it doesn't ask again. Background auto-refresh (from the hook/statusLine) stays off until that first foreground command succeeds, so you never get a surprise prompt mid-session.Token safety: the OAuth token is only ever sent to Anthropic (
*.anthropic.comover https) or loopback — even ifAGENT_GUARD_USAGE_URLis set, an off-allowlist host is refused, so a poisoned env var can't exfiltrate it. SetAGENT_GUARD_NO_KEYCHAIN=1to forbid Keychain reads.
🟢 Claude Code plan limits · observed just now
[██░░░░░░░░░░░░░░░░░░] 5-hour limit 12% used, resets 9:19 AM
[███░░░░░░░░░░░░░░░░░] weekly limit 17% used, resets Tue 7:59 PM, ~83% left over 5.0d (~17%/day vs ~14%/day budget)
[░░░░░░░░░░░░░░░░░░░░] weekly · Sonnet 1%The weekly line spells out the daily budget — your weekly cap ÷ 7 (≈ 14%/day) — and how much runway you actually have, so a number like 17% (or even 60%) reads as "days left", not alarm.
Never a stale number. If the feed stops updating, agent-guard says so instead of quoting an
old reading — a 10-hour-old "0%" is indistinguishable from a real 0% but is simply wrong. A
snapshot older than 30 minutes renders as 🛡 ⚪ limits stale (…), is never graded into a
warning level, and never fires a pacing nudge.
Alternative: the proxy
Anthropic also reports your standing on every response via anthropic-ratelimit-unified-*
headers (5h + weekly only — no per-model). Run Claude Code through the proxy and agent-guard
reads them in-flight:
agent-guard proxy # meters Anthropic + reads limit headers
ANTHROPIC_BASE_URL=http://localhost:8787 claudeOnce those headers are seen, the session is in subscription mode: alert-only. agent-guard never blocks a flat-fee plan (you already paid; Anthropic's own limit is the real wall) — instead it paces you. For each window it computes burn-rate vs. a sustainable pace (the daily budget = weekly cap ÷ 7 ≈ 14%/day) and projects whether you'll exhaust the window before it resets. Crucially the warning is pace-aware: 60% of the weekly cap with two days left is under the daily budget, so it stays quiet — it only warns when you're at or above the pace, or projected to lock out. The danger level is pace-gated the same way: 89% of the cap with 8 hours left, under pace, won't lock out, so it stays calm rather than screaming red — high utilization while under pace only happens near reset, where a lockout can't land (a real projected lockout still escalates regardless of pace). When it does fire, it says so in-session and via your alert channels:
🟥 Claude Code plan limits · observed just now
[████████████░░░░░░░░] weekly limit 62% used, resets Sat 6:00 PM,
~38% left over 5.4d (~7%/day vs ~14%/day budget),
burning 3.1× pace, → lockout in ~14h (5.1d before reset)status shows it; the hook injects it into the session even when only the hook is running
(it reads the snapshot the proxy persisted).
Without the proxy, real percentages are unknowable — Claude Code fetches them from Anthropic
and never writes them to disk, and local cost does not map to Anthropic's internal rate-limit
units (so we don't fake a "% of limit"). Instead, hook-only mode shows what's honestly knowable:
your auto-detected plan tier (read from ~/.claude.json) plus absolute rolling cost —
and points you at the proxy for the real numbers. Your tier needs no flag; --plan only
overrides the auto-detection.
ks guard config --plan max5 # auto (detect) | pro | max5 | max20Tune the thresholds (0–1 utilization) if the proxy's pacing is too eager:
| Setting | Meaning | Default |
|---|---|---|
| --plan (AGENT_GUARD_PLAN) | auto (detect from ~/.claude.json) or pin a tier | auto |
| --weekly-soft / --weekly-danger | weekly warn / danger utilization | 0.6 / 0.85 |
| --5h-soft / --5h-danger | 5-hour warn / danger utilization | 0.7 / 0.9 |
| --burn-ratio | pace multiplier that triggers a warning | 1.5 |
Both the soft and danger thresholds are pace-gated: once a reset time is known, crossing either only escalates when you're at or ahead of the prorated daily budget (weekly ÷ 7), or projecting a lockout — so being deep into the week's quota with little time left doesn't cry wolf (89% with 8h left, under pace, stays calm). A genuine projected lockout still escalates regardless of pace.
The first time the proxy sees the unified-* headers it writes the raw values once to
~/.kill-switch/agent-guard/events.jsonl (kind: "unified-headers-observed") — so you can
confirm Anthropic's exact value formats with a single cat. Only unified-* headers are
captured (an explicit allowlist — never Authorization / x-api-key / cookies), values are
length-capped, and the dump stays local. In auto mode the dollar-wall suppression trusts the
upstream's headers; pin --plan if you'd rather it not depend on what the upstream reports.
Because subscription mode is alert-only, the "don't run both hook and proxy" caveat below doesn't bite here — running Claude Code through the proxy is exactly what feeds the limit headers, and dollars no longer gate anything.
Alerts
On the first soft/hard trip per scope, agent-guard:
- appends an event to
~/.kill-switch/agent-guard/events.jsonl(local audit trail), - posts to Slack if
KILL_SWITCH_SLACK_WEBHOOK(orconfig --slack-webhook) is set, - reports to the Kill Switch dashboard if
KILL_SWITCH_API_KEYis set, so agent kills sit alongside your cloud-account kills.
All network alerts are best-effort with a short timeout — a down endpoint never delays the agent.
Pricing
Built-in rates for current Claude and OpenAI models (USD/1M tokens), including Anthropic
cache multipliers (cache write = input × 1.25, cache read = input × 0.10) — the buckets a
context-replaying agent loop hits hardest. Unknown models fall back to premium Sonnet-class
rates so the guard never under-counts. Override any model in
~/.kill-switch/agent-guard/pricing.json.
Commands
agent-guard install [--global] [--command <cmd>] wire the Claude Code hook
agent-guard proxy [--port 8787] [--flavor anthropic|openai] [--upstream URL]
agent-guard status [--json] spend vs budget + plan limits
agent-guard config [--session-hard N ...] view/set caps
agent-guard config [--plan max5 --weekly-soft 0.6 ...] view/set plan limits
agent-guard reset [--all|--limits|--today|--session <id>] clear the ledger / subscription-limit state
agent-guard hook (internal) Claude Code entrypointHow it fails
By design, the hook fails open: any internal error → exit 0, the agent proceeds. A buggy guard must never brick your session. The proxy fails open too (on a metering error it still relays the response) but the budget check itself fails closed at the 402 wall.
MIT · part of Cloud Kill Switch
