A spend firewall for AI agents โ prices a cloud command before it runs and blocks the over-budget ones.
Project description
๐ฆ budgie
A spend firewall for AI agents. budgie is a Claude Code hook that prices an agent's cloud command before it runs โ folding in storage, node groups, whole loops, and the session's cumulative burn, not just the headline instance โ and blocks the ones that breach your budget. Billing alerts fire after the money's gone; budgie stops the command at the door.
Live: the agent tried to launch 3ร c5.4xlarge (~$1,489/mo) and budgie blocked it before it ran โ over the session's cap. One command puts the gate in front of a real agent:
budgie wrap claude # launches Claude Code with budgie guarding every command
Demo โ the depth is in what it sees
A cheap db.t3.micro blocked not for the instance but for the 20 TB of
storage attached to it โ the difference between "block the big box" and
understanding what a command actually costs.
$ budgie check 'aws rds create-db-instance --db-instance-class db.t3.micro --allocated-storage 20000 --storage-type gp2'
[BLOCK] 1ร rds db.t3.micro โ $3.17/hr ($2,312/mo) โ over the $2.00/hr session cap. Blocked. [+20000GB gp2]
$ budgie check 'aws eks create-nodegroup --instance-types m5.24xlarge --scaling-config desiredSize=40'
[BLOCK] 40ร eks m5.24xlarge โ $184.32/hr ($134,554/mo) โ over the $2.00/hr session cap. Blocked.
$ budgie check 'aws ec2 run-instances --instance-type p5.48xlarge --count 2'
[BLOCK] 2ร ec2 p5.48xlarge โ $196.64/hr ($143,547/mo) โ over the $2.00/hr session cap. Blocked.
$ budgie check 'aws ec2 run-instances --instance-type t3.micro'
[ALLOW] โ $0.01/hr โ within budget.
$ budgie session
agent-8f2: burning $4.61/hr ยท accrued $13.83 so far
Regenerate the animations with vhs:
vhs demo/demo.tape (the CLI view) and vhs demo/wrap.tape (the live agent) โ the
latter records a real budgie wrap claude session; the committed gif is trimmed and
palette-optimised from that raw capture.
Architecture โ a gate and a ledger, kept apart
budgie is two hooks with one discipline between them: anticipate to decide, account to record โ and never let the two cross.
agent (Claude Code) โโ Bash: aws / terraform / gcloud โฆ
โ
โโโโโโโโโโโโโโโโโ PreToolUse โโโโโโโโโโโโโโโโโโโ the GATE โ anticipation
โ budgie hook โ prices a resource that
โ parse every aws cmd (loops, &&, ;, xargs)โ doesn't exist yet; the
โ price static table | AWS Price List API โ estimate is used only to
โ gate accrued + running + this cmd > cap?โ decide, then discarded โ
โ โโ over โ exit 2 โ blocked (never runs) it commits NOTHING
โ โโ under โ allow (exit 0), command runs โ
โ โผ
โ session ledger ($BUDGIE_HOME)
โ active_rate $/hr ยท accrued_cost $
โ โฒ
โโโโโโโโโโโโโโโโโ PostToolUse โโโโโโโโโโโโโโโโโโ the LEDGER โ accounting
budgie posthook fires ONLY on success, so
create succeeded โ COMMIT its cost + record id only real spend is ever
delete succeeded โ credit the cost back written down
* pricing via AWS Price List API is opt-in: pip install "budgie-firewall[aws]"
Why the split matters. The gate anticipates โ it prices a command that
hasn't run and may be blocked, fail, or be a dry-run, so it must not write
anything down. The ledger accounts โ Claude Code fires PostToolUse only when a
command succeeds, so a create counts only once it's real; a failed create
fires no hook and never touches the total. (Verified live on Claude Code 2.1.121:
a failed command fires no PostToolUse โ and no PostToolUseFailure either.)
Keeping a prediction out of the factual ledger is what stops phantom spend โ
money that was never spent โ from wrongly blocking later, legitimate commands.
The gate itself (command โ parse โ price โ verdict) is a pure function over the
stdlib. Pricing and the ledger are seams around it.
Why
Unsupervised agents now run real aws / terraform / gcloud commands โ and
they loop, retry, and over-provision. One went viral for running up a $6,531
AWS bill. Every existing guardrail is too late or too coarse:
- Billing alerts / FinOps dashboards (Vantage, CloudZero) โ report after the spend.
- AWS Budgets โ fires after cumulative spend crosses a line, org-wide, blocks a whole service.
- Infracost โ great, but CI/PR-time and Terraform-only.
- Token-budget tools (Waxell, loop-cost) โ cap LLM tokens, not infra.
None block the specific money-spending command, at agent runtime, before it runs. That's budgie.
The decision
agent about to run: aws ec2 run-instances --instance-type p5.48xlarge --count 2
โ
โผ PreToolUse hook (budgie) โ no execution, no network
parse โ find every aws command (loops, &&, ;, xargs, /path/aws)
price โ 2 ร $98.32/hr = $196.64/hr ($143,547/mo)
gate โ dollars spent + running + this command > the cap?
โ
โโ yes โ exit code 2 โ Claude Code blocks it; the command never runs
โโ no โ allow (exit 0); the command runs, and PostToolUse commits its
cost to the session โ only once it has actually succeeded
budgie inspects and decides allow/deny โ it never runs the command itself (that stays with the agent, only if budgie allows). A hard block uses exit code 2 (the version-proof deny); a warn is a non-blocking hint; allow is silent. No AWS SDK, no network on the decision path.
Session accrual (cumulative cost)
budgie keeps two separate numbers per session โ the current burn and the money already spent โ because a rate is not a cost:
active_rate($/hr) โ what the session is burning right now. A create raises it; a teardown lowers it.accrued_cost($) โ the time-integral of the burn: the area under the active-rate line.
on every event (create / teardown):
accrued_cost += active_rate ร (now โ last_ts) # ฮt capped at 24h
last_ts = now
create โ active_rate += resource_rate
teardown โ active_rate โ= resource_rate # past dollars stay accrued
active_rate $/hr
5 โ โโโโโโโโโโโโโโโโ
โ โ burning $5 โ
3 โ โโโโโโโโโโ โโโโโโโโโโ โ teardown A: rate drops,
โ $3 โ $5 $2 โ accrued does NOT
0 โผโโโโโโโโโ โโโโโโโโโโโโถ time
create A create B teardown A
accrued = ฮฃ (rate ร ฮt)
Both are enforced, and you choose which:
BUDGIE_HOURLYchecksactive_rate(max concurrent burn) โ two individually-cheap boxes can still trip it together.BUDGIE_BUDGETchecks the cumulative total โaccrued_cost(real dollars spent) plus what's still running, projected overBUDGIE_HORIZON. Because torn-down resources keep their spent dollars inaccrued_cost, create/tear-down churn is counted, not laundered.
budgie session prints both. This mirrors KML's cost_estimator accrual โ rate
and cost kept distinct, with active vs torn-down resources tracked separately.
Install
Name: the PyPI package is
budgie-firewalland the command isbudgie. (The barebudgiename on PyPI is an unrelated 2015 SSH tool โ not this project.)
# from PyPI (once published) โ package is budgie-firewall, the command is budgie
uvx --from budgie-firewall budgie check "aws ec2 run-instances --instance-type p5.48xlarge --count 2"
# [BLOCK] 2ร ec2 p5.48xlarge โ $196.64/hr ($143,547/mo) โ over the $2.00/hr cap.
# or straight from GitHub, no PyPI needed:
uvx --from git+https://github.com/TanishkaMarrott/budgie budgie check "aws ec2 run-instances --instance-type p5.48xlarge"
Fastest path โ no settings edit: budgie wrap claude launches Claude Code with
both hooks injected for that session (via its --settings channel), so you can try
budgie in one command.
To wire it in permanently, add both hooks โ PreToolUse blocks before the spend, PostToolUse commits it after success (required for cumulative budgets):
// .claude/settings.json
{ "hooks": {
"PreToolUse": [ { "matcher": "Bash", "hooks": [
{ "type": "command", "command": "budgie hook" } ] } ], // the gate โ blocks (exit 2)
"PostToolUse": [ { "matcher": "Bash", "hooks": [
{ "type": "command", "command": "budgie posthook" } ] } ] // the ledger โ commits succeeded spend
} }
Two caps, use either or both:
BUDGIE_HOURLY=2.0โ a rate ceiling: never let the session burn faster than $2/hr at once.BUDGIE_BUDGET=1.0โ a cumulative total: the net sum of dollars already spent plus everything still running (projected overBUDGIE_HORIZON, default 1h) may never cross $1. This is what catches slow accrual and create/tear-down churn that a rate cap alone misses.
No silent allow. Anything that provisions but can't be priced โ an un-enumerated
service, an unknown SKU, a hidden config โ warns, never falls through to allow;
a dynamic quantity (--count $N) or an unbounded loop blocks. Flip BUDGIE_STRICT=1
to turn every can't-price warn into a hard block (the zero-escape-boat posture).
Configuration
Every knob is an environment variable; all are optional.
| Variable | Default | What it does |
|---|---|---|
BUDGIE_HOURLY |
2.0 |
Rate ceiling ($/hr) โ the session may never burn faster than this at once. |
BUDGIE_BUDGET |
off | Cumulative total ($) โ dollars spent + still-running, projected over the horizon. Needs both hooks wired. |
BUDGIE_HORIZON |
1.0 |
Hours the running rate is projected over for BUDGIE_BUDGET. |
BUDGIE_STRICT |
off | 1 escalates every can't-price warn โ block โ nothing unpriceable runs. |
BUDGIE_FAIL |
closed |
On internal error / corrupt state: closed blocks spend, open allows through. |
BUDGIE_OK |
off | 1 overrides the gate for the command (also .budgie/allow.txt, a substring allowlist). |
BUDGIE_PRICING |
static |
static = bundled offline table; aws = live Price List API (pip install "budgie-firewall[aws]"). |
BUDGIE_HOME |
.budgie |
Directory for the session ledger + price cache. |
Commands
budgie check "<command>" # price + gate one command
budgie hook # PreToolUse hook entry โ prices + gates (exit 2 blocks)
budgie posthook # PostToolUse hook โ commits succeeded spend, credits deletes
budgie tf-plan plan.json # price a `terraform show -json` plan
budgie session # show each session's burn ($/hr) and accrued ($)
budgie ledger # recent decisions + total spend stopped
budgie wrap claude # launch Claude Code with budgie's hooks injected (no settings edit)
What it does today
- โ
Composite pricing โ RDS = instance + storage + Multi-AZ; EKS = control
plane + node groups; EBS volumes ($/GB-mo). Plus EC2, NAT, ElastiCache,
Redshift, ELB, SageMaker, OpenSearch. Warns (never silent-allow) when cost is
hidden โ Fargate, Aurora, EMR, MSK,
--cli-input-json,terraform apply. - โ
Extracts every
awsinvocation from loops /&&/;/ xargs / full paths, including ones behind leading global flags (aws --region โฆ --profile โฆ ec2 run-instances) and inside$(โฆ)/ backticks โ the common forms that must not slip past. Disambiguates service-ambiguous actions: onlyeks create-clusteris the flat $0.10 control plane;kafka/emr create-clusterwarn (bill-shock),ecs create-clusteris free. Handles--dry-run, spot (~70% off), region, robust--count 1:5. - โ
Prices the whole loop, not one iteration โ
for i in $(seq 100); do โฆis costed as 100ร, so the $6,531 runaway-loop pattern blocks. A loop with no bounded count (while/xargs/ a dynamic range) that creates billable resources is refused outright โ cost can't be bounded. - โ Blocks via exit code 2 (version-proof hard deny); warns are non-blocking hints; allow is silent. Crash-proof, fail-closed on spend commands.
- โ
Two enforced caps โ a rate ceiling (
BUDGIE_HOURLY, $/hr) and a cumulative total (BUDGIE_BUDGET, $): the net sum of dollars spent + running, projected over a horizon. Rate and accrued cost are tracked separately (the KML model);budgie sessionshows both. - โ
PostToolUse accounting โ commits a create's cost only once it succeeds,
records the resource id, and credits it back when the agent deletes it. A
failed create fires no hook, so it never counts โ and as a belt-and-braces check it
also refuses to commit an interrupted command or one whose output carries a
botocore error, so no phantom spend even if a future version fires PostToolUse on
failure. Resources are recorded by their user-assigned identifier too, so a
teardown still credits back under
--output text. A resize (modify-*) is gated but not re-committed (no double-count). No AWS polling. - โ
Durable session state โ the budget ledger is written atomically
(
os.replace) and guarded by a file lock, so a crash mid-write or two parallel hooks can't corrupt it or lose a commit. A ledger that can't be read fails closed (blocks spend) rather than silently resetting the budget to $0. - โ
Terraform โ
budgie tf-planprices aterraform show -jsonplan. - โ
Ledger + override (
BUDGIE_OK=1/.budgie/allow.txt). - โ Live AWS pricing โ region-aware, disk-cached (AWS Price List API); zero-dep static table is the default.
Roadmap
- EC2 root/data volumes โ fold
--block-device-mappingsEBS into the instance estimate (RDS storage + EKS nodes are already composite). - Out-of-band deletes โ reconciliation is agent-driven; console / TTL / autoscaling deletes would need resource-snapshot polling (a platform's job).
- Multi-cloud โ GCP / Azure pricing tables.
- Hard enforcement โ mint a scoped credential / SCP from the budget (the un-bypassable tier).
Limitations (honest)
budgie is a fast advisory guard, not an un-bypassable control. Know its edges:
- Bash-tool only. An agent using an AWS MCP server (boto3 directly) bypasses it. The un-bypassable tier is a scoped credential / SCP minted from the budget โ on the roadmap.
- Cumulative budgets need both hooks.
BUDGIE_BUDGETaccrues through the PostToolUse hook, so wire bothbudgie hookandbudgie posthook(the setup above does). PreToolUse alone still enforces the per-command rate cap, but won't accumulate spend across commands. - EKS nodegroup counts
desiredSize(or 1 when onlymin/maxis given); autoscaling beyond desired isn't priced. - Composite only where the data's in the command. RDS storage/Multi-AZ and EKS
node groups are folded in; an EC2 instance's own EBS volumes
(
--block-device-mappings) are not yet, so a big root volume can be under-counted. The cumulative session total partly compensates. - Agent-driven reconciliation. budgie credits deletes it sees (the agent's own commands). A delete done from the console / by TTL / by autoscaling won't be caught without snapshot polling โ out of scope for a hook.
- Provisioning cost, not usage cost. Usage-based services โ S3, Lambda, DynamoDB on-demand, data transfer, NAT data processing โ can't be known before the fact and are not priced.
- Estimates, not invoices. It ignores Reserved Instances / Savings Plans; spot is a rough ~70% off. A covered account may see over-estimates.
- Curated pricing, but no silent allow. The static table prices the big bill-shock services exactly; anything else that provisions (an unknown SKU, an un-enumerated service, a hidden config) still warns rather than passing through โ only genuinely free creates (security groups, tags, IAM, log groupsโฆ) are silent. So coverage gaps cost you a warning to review, never an unnoticed create.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file budgie_firewall-0.1.1.tar.gz.
File metadata
- Download URL: budgie_firewall-0.1.1.tar.gz
- Upload date:
- Size: 4.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d7408950e509c78f0a4b7d3c65cb175a73b5e6f8160b4d7a5a0c948e7913785
|
|
| MD5 |
ed81c3b3a861958949695bb2236817b1
|
|
| BLAKE2b-256 |
c6932c0f927e47f7fd4e2a157a4e7e6136659f407edb71c8da844665dacddd95
|
Provenance
The following attestation bundles were made for budgie_firewall-0.1.1.tar.gz:
Publisher:
publish.yml on TanishkaMarrott/budgie
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
budgie_firewall-0.1.1.tar.gz -
Subject digest:
1d7408950e509c78f0a4b7d3c65cb175a73b5e6f8160b4d7a5a0c948e7913785 - Sigstore transparency entry: 2246042112
- Sigstore integration time:
-
Permalink:
TanishkaMarrott/budgie@109a1cc450a5d501e2fcf92f3c38d7018b10917b -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/TanishkaMarrott
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@109a1cc450a5d501e2fcf92f3c38d7018b10917b -
Trigger Event:
push
-
Statement type:
File details
Details for the file budgie_firewall-0.1.1-py3-none-any.whl.
File metadata
- Download URL: budgie_firewall-0.1.1-py3-none-any.whl
- Upload date:
- Size: 38.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c9cb43185dda004514834a2f08f27aab7e319609c5465eb086f8305ae0165d4
|
|
| MD5 |
5a545c682e0c2f571f3f7df01ebb4578
|
|
| BLAKE2b-256 |
4b7fd2472d64c813a1d28e72b45d46d4fd0d743c8a19fd0fcd66a568130f5f8c
|
Provenance
The following attestation bundles were made for budgie_firewall-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on TanishkaMarrott/budgie
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
budgie_firewall-0.1.1-py3-none-any.whl -
Subject digest:
0c9cb43185dda004514834a2f08f27aab7e319609c5465eb086f8305ae0165d4 - Sigstore transparency entry: 2246042827
- Sigstore integration time:
-
Permalink:
TanishkaMarrott/budgie@109a1cc450a5d501e2fcf92f3c38d7018b10917b -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/TanishkaMarrott
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@109a1cc450a5d501e2fcf92f3c38d7018b10917b -
Trigger Event:
push
-
Statement type: