Hard spend caps and a kill-switch for the non-LLM actions your AI agent takes.
Your LLM gateway meters tokens. It does not see the aws ec2 run-instances your
agent just ran.
breakerbox sits in Claude Code's PreToolUse hook, reads every Bash command
before it executes, estimates what it will cost, and blocks it if that breaches
a cap you set. No proxy, no daemon, no credentials, no account.
Live site & demo → · How it works
$ breakerbox check "aws ec2 run-instances --instance-type p4d.24xlarge --count 8"
DENY estimated $6292.34
Billable actions
EC2 p4d.24xlarge x8 $6292.34
aws.ec2.run-instances · $32.7726/hr · qty 8 · high confidence
Why
- Single action estimated at $6292.34, over the per-action cap of $20.00.
- Session spend would reach $6292.34, over the session cap of $50.00.
The well-documented agent-runaway incidents share a shape: the money was not burned on tokens. An agent looped, spun up EC2 instances and CloudFormation stacks on its own, and left its operator with a ~$6,500 bill. Every dollar of that was invisible to an LLM gateway, because not one of those calls was a model call.
Existing tools cover the model call. LiteLLM, Portkey, Bifrost and friends do session budgets, iteration caps and alert→throttle→kill on token spend, and they do it well. Cloud-native budgets (AWS Budgets, billing alarms) are reactive — they tell you hours later, once the money is gone.
Nothing was watching the gap in between: the shell command, the cloud SDK call,
the terraform apply. That gap is what breakerbox covers.
breakerbox is not a LiteLLM competitor. Run both. LiteLLM caps what your agent spends on tokens; breakerbox caps what it spends on everything else.
npm i -g @shopdevx/breakerbox
breakerbox initinit writes breakerbox.config.json, creates .breakerbox/, and registers two
hooks in .claude/settings.json. Restart Claude Code and it's live.
breakerbox doctor # verifies the whole chain, including a real blockdoctor doesn't just check that files exist — it feeds a synthetic 8×p4d.24xlarge
launch through the actually-registered hook command and asserts it comes back denied.
Why npm i -g rather than npx
The hook runs on every Bash tool call, so startup cost is paid constantly.
breakerbox has zero runtime dependencies and no build step for this reason —
a hook evaluation is ~48ms median on a warm machine, essentially all of it Node
process start. Routing that through npx each time would multiply it, and npm
prunes the _npx cache out from under the registered path. init warns you if
it detects it's running from there.
| Shape | Example | Result |
|---|---|---|
| Oversized resource | aws ec2 run-instances --instance-type p4d.24xlarge --count 8 |
deny — $6,292 > per-action cap |
| Runaway loop | while true; do aws ec2 run-instances ...; done |
deny — billable action in an unbounded loop |
| Accumulated drift | 30 small launches across one session | deny once the session cap is reached |
| Burst velocity | 12 billable actions in 60s | deny — that's a loop, not deliberate work |
| Opaque blast radius | terraform apply -auto-approve |
ask — cost lives in files, not the command |
| Unattended opaque action | same, in bypassPermissions mode |
deny — an "ask" nobody can answer isn't a guardrail |
| Hidden in substitution | ID=$(aws ec2 run-instances ...) |
deny — substitutions are parsed too |
| Ordinary work | npm test && git commit -am wip |
allow, silently |
Covered today: AWS (EC2, RDS, EKS, SageMaker, Redshift, ElastiCache, OpenSearch, MSK, CloudFormation, ASG, NAT/ALB/TGW, IAM/Organizations), GCP (Compute, GKE, Cloud SQL, Dataproc, Cloud Run, Deployment Manager), Azure (VM, VMSS, AKS, managed DB, ARM/Bicep), IaC (Terraform, OpenTofu, Pulumi, CDK, SAM, Serverless, Helm), plus metered HTTP APIs and rented-GPU CLIs.
Two numbers per action: oneTime and hourly.
This matters more than it sounds. Launching an EC2 instance costs $0 at the
moment you launch it — the bill arrives over the following hours. A guardrail
that only counted immediate cost would never fire on the exact command that
caused the incident. So breakerbox projects hourly over a horizon (default
24h) and charges that against your cap up front:
charged = (oneTime + hourly × horizonHours) × quantity × loopIterations
Unknown instance types resolve to a deliberately pessimistic rate and report low confidence — a guardrail that guesses low is worse than no guardrail. GPU families are detected by name and default higher still.
For terraform apply and cloudformation create-stack, breakerbox refuses to
invent a number at all. The cost is defined by files it isn't reading, so it
reports unknownBlastRadius and escalates instead of pretending to price it.
terraform apply is opaque at the command line — which is why the live hook
returns unknownBlastRadius and asks. But once you render the plan to JSON, the
priced diff is right there. breakerbox plan reads it and gives the plan the same
DENY/ASK treatment a command gets:
terraform plan -out tfplan
terraform show -json tfplan > plan.json
breakerbox plan plan.jsonDENY estimated $1197.32
Priced resources
aws_instance.trainer $786.54 tf.aws_instance · $32.7726/hr · p4d.24xlarge
aws_eks_node_group.gpu $408.38 tf.aws_eks_node_group · $5.6720/hr · qty 3 · g5.12xlarge
aws_eks_cluster.main $2.40 tf.aws_eks_cluster · $0.1000/hr
Coverage
Priced 3 of 4 created resource(s); 1 unpriced (no rule yet).
Only resources whose plan action includes create are billed — that covers fresh
creates and replaces; updates and deletes are left alone. It reuses the same price
catalog and 24h horizon as the command engine, and reports how many resources it
couldn't price rather than guessing. This is an opt-in step: it is deliberately
not wired into the hook, so the hot path stays pure string-parsing with no
subprocess and no Terraform dependency. Run it in CI before apply, or by hand.
PreToolUse writes a pending intent. PostToolUse promotes it to a committed
charge. Nothing counts against your caps until the tool has actually run.
Without this split, a command you denied — or the user cancelled at the permission prompt — would still eat the session cap, and a few blocked commands could lock out an agent that never spent a cent.
breakerbox.config.json in your project root:
Env overrides: BREAKERBOX_SESSION_CAP, BREAKERBOX_DAILY_CAP,
BREAKERBOX_DISABLE=1, BREAKERBOX_CONFIG, BREAKERBOX_DIR.
breakerbox check "<command>" # dry-run the policy engine, explain the verdict
breakerbox plan <plan.json> # spend preflight for a Terraform plan (terraform show -json)
breakerbox status # spend against every cap, with meters
breakerbox log -n 20 # recent committed actions
breakerbox reset --session # clear a session's spend (--day, --all)
breakerbox doctor # verify the install end to endRead this part. A guardrail that oversells itself is worse than none, because you stop watching.
- It is a spend guardrail, not a security sandbox. It reads the command line.
eval "$(echo YXdzIGVjMi4uLg== | base64 -d)", a cost-incurring action inside a shell script it invokes, or a Python SDK call insidepython deploy.pyare all invisible to it. It defends against runaway agents, not against an adversary deliberately evading it. - Prices are approximate list prices (
src/catalog/prices.js, stamped2026-08). No region adjustment, no reserved instances, no savings plans, no spot, no committed-use discounts, no data transfer or storage. They exist to make caps trip at roughly the right time, not to reconcile your invoice. - Only
Bashis inspected today. MCP tool calls and other tools pass through. failModedefaults toopen. If breakerbox itself errors, the command proceeds and the error lands in.breakerbox/errors.log. A bug in this tool should not brick your agent. Set"failMode": "closed"to invert that.allowis expressed as silence. breakerbox never emitspermissionDecision: "allow", because that would bypass Claude Code's own permission checks and auto-approve commands it merely had no opinion about. It only ever speaks up todenyorask.- Hourly resources are charged once, at launch. breakerbox does not track whether you later terminated the instance. The 24h horizon is a heuristic for "is this worth stopping", not an accrual system.
breakerbox is a backstop, not the only line of defence. The strongest setup pairs
it with least privilege: during phases that don't need cloud access, strip raw
Bash from Claude's tool list (--allowedTools) so no cloud command can be issued
at all. If the guard never has to evaluate a command, it can never fail open on one.
Use breakerbox for the phases where the agent legitimately needs a terminal.
When someone wakes up to a surprising bill, three outcomes look identical from the outside: the guard ran and priced it under the cap, the guard ran and crashed into fail-open, or the guard was never invoked at all. Tests prove the code does the right thing in each case — they don't tell you which case you were in on the day.
So breakerbox appends one line per evaluated command to .breakerbox/decisions.jsonl:
the command, the verdict (allow/ask/deny), and which failMode was live. A
crash is recorded as outcome: "evaluation-error" rather than vanishing. And the
absence of a line during the incident window is itself the signal that the hook
wasn't wired. It's cheap, local-only, and doubles as the artifact you hand to whoever
asks why the bill looks like that.
- MCP tool-call interception (
mcp__*matchers) - A
PreToolUsematcher forWrite/Editon IaC files, to catch spend at authoring time rather than apply time - Framework adapters beyond Claude Code (LangGraph, CrewAI) via a generic subprocess wrapper
- Auto-trigger the Terraform preflight when
terraform apply <saved-plan>is seen at the hook (todaybreakerbox planis opt-in; see "Spend preflight for Terraform") cloudformation/ CDK / Pulumi plan parsing, the same way the Terraform preflight works
npm test # 67 tests, node:test, no dependenciesMIT © ShopDevX
{ "caps": { "action": 20, // any single command line "session": 50, // one agent session "daily": 200 // per UTC day }, "rateLimit": { "actions": 12, "windowSeconds": 60 }, "horizonHours": 24, // hours of runtime charged up front "unboundedLoopAssumption": 25, // iterations assumed for `while true` "onBreach": "deny", // deny | ask "onUnknownBlastRadius": "ask", // terraform apply, CFN stacks "onUnboundedLoop": "deny", "unattendedEscalation": "deny", // ask -> this, in bypassPermissions "unmatched": "allow", // commands with no rule "allow": ["aws s3 ls"], // substring or /regex/ "deny": ["/aws\\s+organizations/"], "ignoreRules": ["aws.s3.transfer"], "priceOverrides": { "aws.ec2.run-instances": { "hourly": 0.2 } }, "failMode": "open" // open | closed }