Budgets, caps and a kill-switch for unattended runs
Three caps around a run nobody is watching — the provider's, the harness's own, and a watchdog you write — and the cheap test that proves each one fires.
The stop condition you just removed
When you sit and watch a run, you are its stop condition. The agent starts rewriting a file it shouldn't and you hit escape. You didn't configure that, you just noticed — and it's doing more work than anything in your settings file
Unattended, that's gone, and what's left is whatever you wrote down beforehand. Which matters because the expensive failure here isn't a wrong answer — a wrong answer costs one turn. It's a loop. A test that won't go green, a tool that keeps erroring, an agent that keeps trying, patiently, at a few cents a turn, for as long as you let it. Nobody plans that run, it's the same run you've done fifty times, on the night the thing it depends on is broken
If you've never had a bill like that, it's not luck — it's that you were sitting there. The chapter on fan-out and the one on headless runs both remove you from the loop, which is the change that makes this worth half an hour
Three caps, and only one of them survives your own mistakes
There are three places where you can place the ceiling — in the provider account (as a spend limit on it, or its workspace, seat or budget), in the harness (as a flag or a config key in dollars, credits, steps or timeout) and at the process level (via an external watchdog). They all work differently:
The provider limit is usually set up on the billing account level, but can also be configured for a workspace, seat or budget. It's really slow and crude, so it's not very effective, but — being external to the run — works even if the script fails, you miss a flag or invoke the thing manually
The harness flag or config key is much more elegant and powerful, but bound to the run where you set it (unless you hardcode it)
An external watchdog is the only way that doesn't require the agent to be intelligent enough to police itself
Make sure to apply them in this order — starting with the provider limit which is cheap (costs minutes) and universal (for all future runs) and ending with the other two, which are more sophisticated but apply only to the current run
It's not just a good practice, it's vital. We've seen many people setting up a nice wrapper script and setting a flag for it, but forgetting about the provider limit. In such a scenario anything that doesn't use the wrapper isn't bound by any spend limit
Pick the number before the run, in the unit the tool actually bills in
Whichever you choose, make sure to decide on the figure before you run it, in the tool's native unit — if it's a low-budget thing, worth like a coffee, reaching a cost of a lunch means it's already a mess. So just note the figure (in dollars, credits, steps or minutes) and set it somewhere the harness understands — this way:
Claude Code accepts it in dollars
Copilot CLI takes it in credits
Kimi Code counts steps, and Antigravity CLI minutes
Codex CLI and Cursor have no such parameter at all
If you can't configure the ceiling in the harness itself, translate it to time and give it to a watchdog — every run bounded at 20 minutes is also bounded by spend (and as long as you've read the sibling lesson on reading usage, you know the per-minute pace)
The caps usually work per invocation, not per fleet. So if you set up a $2 ceiling and run it with twelve agents, you'll have twelve runs at $2 each (costing $24), not one run for $2
The cap can't stop the turn that breaks it
A budget can't stop the first turn that exceeds it — we only know how much a turn costs once it returns (request, response, pricing, comparing, stopping). So the first turn that goes over the limit is already paid. GitHub says it plainly in Copilot CLI's own help — "AI credit usage is known only after a model call returns, so one call can exceed the limit before the CLI blocks the next model call" — and every cap we could measure behaved that way
We've set up a $0.0001 ceiling in Claude Code 2.1.221 and run a single-line prompt — it told us that spent $0.1258625, finishing with "budget_exhausted" and "Reached maximum budget ($0.0001)":
"total_cost_usd": 0.1258625,
"terminal_reason": "budget_exhausted",
"errors": ["Reached maximum budget ($0.0001)"]The cap worked (stopping after the first turn), but cost about a thousand times more than the ceiling, because it was the first run on the big model; the same prompt and limit on the small model finished at $0.000583
This means that for any cap, there's always a floor — the price of one turn, defined by the model and the cache state, not by the cap itself. So they can stop runaway loops (like an overnight retry spree), but not expensive turns (like a full-monorepo read). And if you set up a ceiling lower than the price of a single turn, the only thing it will do is stopping the run with a "budget exhausted" message and fully charging you for it
The kill switch is a second process
That's why the timeout should be defined by an external process. If you set a timeout in the tool, it's just some code inside the thing you're trying to stop; and will work only if the run is healthy, which isn't what we want
You can do it with Antigravity CLI 1.1.9 — there's a --print-timeout flag, and indeed it fires, but for example on this prompt (which answers in a couple of seconds):
$ agy --print-timeout 1s -p "Reply with the single word: ok"
Error: timeout waiting for response
EXIT=1 ELAPSED=5sIt took 5 seconds to hit the one-second cap
While if we run it with the same prompt and no timeout flag, but with a watchdog that stops it after two seconds:
EXIT=1 ELAPSED=2s
Error: context canceledIt actually took two seconds
Six lines of shell is enough to set up the watchdog for any of the tools (including those that don't have a cap at all), but it doesn't use timeout(1) which isn't even available on a stock macOS:
your-agent -p "$PROMPT" &
run=$!
( sleep "$WALL_CLOCK"; kill -TERM "$run" 2>/dev/null ) &
watchdog=$!
wait "$run"; rc=$?
kill "$watchdog" 2>/dev/nullThe point is to define the deadline in another process, so the agent can't affect it
Same with the CI's job timeout — it defines this kind of deadline too, just one level higher. Set it up to something sensible for your agent, as the default value of your runner isn't optimised for it
If you stop a run with a TERM signal, the wrapper usually reports 143 — that's 128 plus the signal number, and TERM is 15. But a harness that catches the signal exits with its own code instead, like Antigravity does with 1. Either way it's not 0, and that's the thing to assert on: otherwise CI reads a half-done run as a pass
Decide now what a kill leaves behind
There's another half of the stopping thing though — the state. While budgets are about money, stopping is about state, and it leaves half of it behind:
The run will always stop mid-edit — some files changed, some not, no commit documenting which ones. It's manageable if you expect it (and plan accordingly), but can be painful if you don't
If you're not in the room, make sure the run is set up to operate in its own branch or worktree, so you can just delete it instead of going through the changed files to check what happened; see the worktree lesson in the fan-out chapter
If you're not in the room, don't do anything that leaves the machine. Don't push, merge, publish or deploy anything — stopping a run doesn't revert the push. And generally speaking, the more automated your workflow is, the stricter guardrails you need to put around it; that's what the guardrails chapter is for
If you're not in the room, make sure that the run creates some record of itself (in a file or in the database), so you can find it and examine; they all can output machine-readable results
Prove the cap fires
Run the thing with an absurdly low ceiling and watch it trip. It costs one turn, and it's the only way to know the difference between a cap and a line in a config file that nothing reads
Well, actually you need to check two things:
It stops, which is obvious and what people check
And it returns with non-zero exit code, which is less obvious, but vital in the headless scenario as otherwise your pipeline will be green over a run that ran out of money halfway
For example if you cap Claude Code 2.1.221 at $0.0001, it will exit with 1 on the budget path, with the subtype being "error_max_budget_usd", and with 0 (and the subtype of "success") if you set it to some sensible value. Make sure your CI asserts on that rather than on the log message
If the cap accepts a number, you can always validate it — so the tool will tell you it's invalid before it even tries to call the model. But if you define it in a config file, the linter should validate the type, so you'll know it's a string instead of a number. The only thing they usually don't check is if it exists (so if you misspell the key, like we did for one of the tools), which might be worth verifying if you place the cap in a config file
In Claude Code
The per-run cap. There's a real one here and it's set in currency, via the --max-budget-usd flag — the help on 2.1.221 describes it as "Maximum dollar amount to spend on API calls" and says it only works with --print.
claude -p "$PROMPT" --max-budget-usd 2 --output-format jsonSo it covers the headless path, which is the unattended one — not an interactive session you walk away from
We haven't found this flag in the "Manage costs effectively" page which is about monitoring and reducing costs rather than capping a single run; but we've found it via claude --help. So, if it comes to limits, you might want to check the output of claude --help too
Reading the result. If you set the output format to JSON using the --output-format json flag, what it returns for a single run is an envelope containing a few properties that can be asserted against. For example, this:
{
"is_error": true,
"num_turns": 1,
"total_cost_usd": 0.1258625,
"terminal_reason": "budget_exhausted",
"subtype": "error_max_budget_usd",
"errors": ["Reached maximum budget ($0.0001)"]
}for a run that hit the cap. If you terminate the run in any way, it still sends the envelope on its way out, but with the reason set to "aborted_streaming". So even if the run is terminated, it still tells how much it cost
Proving it. To verify it, just run this:
claude -p "Reply with the single word: ok" --max-budget-usd 0.0001 --output-format json
echo "exit $?"The exit status will be 1 and the envelope's subtype property will have the value of "error_max_budget_usd"
Just don't pipe the output into head when checking the exit status, it'll print its own status which will result in 0
But what's also interesting is that it's not a hard limit but rather an indicative one. That same command got $0.1258625 spent on the large model. In theory, it shouldn't be higher than $0.0001 but it is. This is a little bit less visible for the small model — we've got $0.000583 there, from the same prompt and the same flag
Where the account cap lives. The most robust limit is the one on account level. The place where it lives depends on the type of account you have:
API organisations — in a workspace (that's an API organisation-specific concept) called "Claude Code" which gets created upon first authentication; you can also go to its Limits page and set a rate limit to not overwhelm your production traffic with an overnight batch
Team and Enterprise plans — by default limited by number of seats, which can be increased by enabling usage credits, after which spend limits can be set on the organisation, group or individual member level via admin settings
Bedrock, Google Cloud's Agent Platform, Microsoft Foundry — in the respective cloud providers' budgets management interface; the docs explicitly say that Claude Code doesn't send metrics from your cloud back to Anthropic, so it's not covered by their analytics dashboards at all
The gotcha. Also, keep in mind that it's a flag which means that it limits only the call on which it's used. For instance, if you were to run 12 headless calls with a $2 cap each, it would be set to $24 in total. We'd rather define this figure in the wrapper script and reference it there instead of defining it 12 times
And last but not least — they say that sessions consume typically less than $0.04 even if idle, so it's a non-zero cost per session and it's counted per session
In Codex CLI
The per-run cap.
The Codex CLI — no run budget ceiling, there is none. You can look at the codex --help and codex exec --help for 0.146.0 to see that neither has a budget flag or credit flag or a token limit or turn limit or timeout. If you were to check the official docs, you'd see they don't say anything about it too, the automation section is about isolation. So there's nothing to pre-empt here, the only safeguard is the watchdog we outlined in the previous part which becomes the ceiling instead of a backup layer. Like:
codex exec --json "$PROMPT" >run.jsonl &
run=$!
( sleep "$WALL_CLOCK"; kill -TERM "$run" 2>/dev/null ) &
watchdog=$!
wait "$run"; rc=$?
kill "$watchdog" 2>/dev/nullObtaining the result — the stdout of the Codex CLI gets converted to JSONL with --json and that's what contains the usage figures (in the turn.completed event). Like:
{"type":"turn.completed","usage":{"input_tokens":24763,"cached_input_tokens":24448,"output_tokens":122,"reasoning_output_tokens":0}}All in tokens, so you need to do the math based on the price list. There's also -o/--output-last-message which lets you output the last message for assertions and --ephemeral which disables the creation of session files (usually best practice for CI). You can also ask it about the rate limit headroom using the /status command in interactive mode.
The account level cap — as there's no run level limit, the only ceiling that matters is the one on the account level. Which is not the part of the CLI but either the plan or the platform account you have it billed to. So make sure it's set to a sensible value before scheduling things, this applies to Cursor as well and none of the other harnesses we've described so far.
The trap — people sometimes mistake flags that sound like limits for actual spend limits. Like -s/--sandbox read-only or -a/--ask-for-approval, they're about permissions and what the model can do, not how much it can cost. Especially if you set --ask-for-approval never for your non-interactive run, then any failures will just get sent back to the model which is great for automation but at the same time it's the most expensive circle you can get yourself into on this harness — it'll keep generating turns without anyone to intervene. It's basically a wall-clock limit that separates such a scenario from an overnight run.
In GitHub Copilot CLI
The per-run cap. You can set a per-run cap — in terms of AI credits — which is opt-in and gets enabled using a CLI flag alongside the prompt; you can also set it mid-session with /limits set max-ai-credits 30, or run /limits on its own to open the dialog
copilot -p "$PROMPT" --max-ai-credits 30 --allow-all-toolsWe think it's quite interesting that its help is so descriptive in terms of what it does and all of it is crucial for non-supervised runs. It says that the minimum is 30 credits as for it any lower value can't be sensible, given it can't ever be enough to make a decent CLI move; similar thoughts about minimums were shared by us at the beginning
It also says outright that it's not strict — "The AI credit limit is a soft cap: usage is known only after a model response returns. A response can therefore exceed or exhaust the limit before the CLI can observe that it has done so; the next model call is then blocked." In such a case it will deny further LLM calls, but if you have a fan-out in a single session (like a few threads), all of them will share the parent's cap so it's just a single number and much more convenient to reason about compared to the per-invocation harnesses. What's more, even the compaction is charged (it uses the same budget) but isn't visible as an assistant message
The second thing is that there's a flag that caps the number of autopilot continuations it sends automatically (5 by default), after which it stops so the non-supervised usage is capped
Proving it. The verification part is the least expensive, given the check happens locally before hitting any LLM.
copilot --max-ai-credits 5 -p "say ok" --allow-all-toolserror: option '--max-ai-credits <credits>' argument '5' is invalid.
Invalid value for --max-ai-credits: "5". Use at least 30 AI credits.If you try to use it with a lower value than 30, you'll get an error that will inform you about it and repeat the minimum of 30 credits; this way you can make sure the flag made it to the CLI and you typed it properly. During the run you'll see some percentage-based info on how it's doing: 50%, 75%, 90%; if you type /usage it will tell you how many AI credits were used so far in the session and break down the tokens
Where the account cap lives. The most secure is a GitHub account-level cap, given it's a real billing thing rather than a toggle like the rest of the harnesses. There are budgets available on user, organisation, cost centre or enterprise level, with one key difference: By default the app just notifies you when a limit is reached and keeps using it (so keeps charging you), so you need to enable "Stop usage when budget limit is reached" for every limit you create. If you set up a budget of $0, nobody it applies to will be able to use it at all, and a user-level budget always hard stops regardless of the settings — there's no option to let usage run past one
The gotcha. The per-run cap doesn't get saved though, it's either a CLI flag or a slash command from a session so there's no config key for it (checked the entire config help), /clear and /new reset the AI credits used but keep the cap you set up so if you start another run it'll have none; to have a cap for future runs you need to add it to your launch script, the only actual cap is the budget
In Cursor
The per-run cap.
2026.07.23-e383d2b — there's no --budget, --credit, --tokens, --timeout or anything of that sort under agent --help. That means the CLI doesn't have a per-run cap, so for agent -p (which is not interactive), the only way to limit the total spent is the wall-clock we mentioned. You can run it and then start a timer that sends SIGTERM after this wall-clock value to it, then wait for it and rm the timer if everything's good
agent -p "$PROMPT" --output-format json >run.json &
run=$!
( sleep "$WALL_CLOCK"; kill -TERM "$run" 2>/dev/null ) &
watchdog=$!
wait "$run"; rc=$?
kill "$watchdog" 2>/dev/nullBut we'd suggest using -w/--worktree with it as it creates a separate git worktree in ~/.cursor/worktrees/ so in such scenario you can just remove this worktree if you don't want the run
Where the account cap lives.
The maximum amount is set in the Spending section of the dashboard. Again, unlike the majority of other caps, this one actually stops the usage. To be able to see or set it you need to turn on the on-demand usage first and then set a monthly spending limit for your account or team (for your team members if you're an enterprise customer). If hit, AI features are no longer available for you and you receive a notification, but they stay available for your teammates. In enterprise, it's the admins who can set limits for particular members or groups under Members and Groups tabs.
It stops the usage so it's a proper stop limit for a runaway process, but also a real stop limit that resets on the next month so if hit you lose your entire day not only the job itself.
Proving it.
To make sure there's really no CLI option for a budget look for --budget under agent --help, and if you don't find it — there's nothing to find so it's good, also have a look at the output of agent status (it shows your identity, no figures) and just have a look at the figure in Spending section of the dashboard. Don't hit it — if you do, you won't be able to use your own tool until the month is over
The gotcha.
-f/--force and --yolo sound like the far end of a dial that has a cap at the other end. They're not — they're permission modes. An unattended agent -p has no spend ceiling below your monthly one, so on this harness the wall-clock number in your wrapper isn't a nice-to-have
In Antigravity CLI
The per-run cap. 1.1.9 of the CLI has only one timeout-related flag — --print-timeout (for the wait time in print mode, set to 5m0s by default). There's no budget, credits or tokens flags. In theory, you can run:
agy -p "$PROMPT" --print-timeout 15m --output-format jsonYou'll want to raise it, because the default of 5 minutes is too low for serious work — as the docs say a run waits maximum of 5 minutes for an answer, so doing a long thing in print mode it will exit after this time (which might be confusing), and this is the first problem most users face.
How exact it is. We'd say that it's very much not precise; that's why we mentioned an external watchdog in the first place. If you run a prompt that usually responds in seconds with this:
$ agy --print-timeout 1s -p "Reply with the single word: ok"
Error: timeout waiting for response
EXIT=1 ELAPSED=5sYou will see: Error: timeout waiting for response and it'll take 5 seconds after the "one second" instruction. It's visible though, that if you were to wait for the same prompt with a 3s ceiling, it would respond in around 6 seconds, so this is about a timeout for waiting for an answer rather than for the entire process. If you run it with the watchdog, it will exit with context canceled after two seconds. The bottom line is — use --print-timeout to kill runs that get stuck, and use the watchdog to actually set the timeout.
Reading the result. In terms of outputs, there are two flavours:
--output-format json— you get a single object at the end containing status, response, error and duration_seconds as well as token usage.stream-json— emits NDJSON events on the way (including the final result) so it's best if you want to monitor the number of tokens used in real time.
Status is one of these values: SUCCESS, ERROR, CANCELED, INTERRUPTED, INVALID, WAITING, RUNNING. The CLI exits with code 0 on success and with non-zero on error (with a message why it happened printed to stderr).
Where the account cap lives. At the account level, there's also a limit in place — quota rather than money. You can use /usage to pull from the backend current quota for your account and show you per-model limits and number of remaining requests or tokens. It doesn't show any amounts though, and you can't set any ceiling here as the plan is the ceiling.
The gotcha. So if the unit is quota, it all comes down to how long you're gonna run things for and what models you'll be using. If you want to spend less money on a long unattended run, just pick a cheaper model.
In Kimi Code CLI
The per-run cap. The maximum number of steps per run is set in the config file — there's no equivalent command-line option for that. The file is located at ~/.kimi-code/config.toml (or config.toml if you have $KIMI_CODE_HOME set to something else):
[loop_control]
max_steps_per_turn = 40
max_retries_per_step = 3
reserved_context_size = 50000For example, in the above config, for each turn of the loop we will do maximum of 40 steps (if we set it to 0 or omit it — there's no limit, so the loop will basically run infinitely). We'll try to run any step maximum of 3 times and will set aside 50000 tokens for the output, in which case the compaction will be triggered when there are less than 50000 tokens left in the LLM's window.
Check that middle key against your own version though. The current docs call it max_attempts_per_step — it got renamed in 0.32.0, counts total attempts including the first one and defaults to 10 — and on 0.31.1 that new name is silently ignored while the old one still works. Whichever release you're on, one of the two names does nothing, and nothing tells you which.
Before running anything unattended you might also want to check these defaults from other sections:
[background]
bash_task_timeout_s = 600[subagent]
timeout_ms = 7200000Which means that for 2 hours per subagent, in a fan-out scenario any hung delegate will survive for this long when the run is being done unattended.
There's no spend limit or a way to set CLI timeout, so the time aspect is still guarded by the watchdog.
Proving it. You can run kimi doctor to check if your config files are in a good shape, pointing it to a copy instead of the live one if you want to:
kimi doctor config ./config-candidate.tomlIt returns exit code 0 if everything is OK and 1 if there's something wrong with a particular key — for example:
Kimi doctor found 1 issue.
ERROR config.toml ./config-candidate.toml
Invalid configuration in ./config-candidate.toml.
Validation issues:
loop_control.max_steps_per_turn: Invalid input: expected number, received stringThe gotcha. The doctor checks only if the type of the values is correct, not if the keys actually exist, so if you put a typo in there and for example create a key called max_steps_per_run (instead of max_steps_per_turn), it will go unnoticed by the doctor:
[loop_control]
max_steps_per_run = 1000Which means that this run would be completely unprotected against endless loops. You can run kimi doctor and it will return exit code 0, telling you everything is fine.
The only way to find out about it is to do the reverse of what we've just shown — set a key with an incorrect type and see if the doctor mentions this key:
[loop_control]
max_steps_per_turn = "1000"And run the doctor pointing it to that file. If you see the key you've set in the output, it means it's actually recognised on your version. If the doctor says everything is OK with this file, it means that no one is reading this key in your version.
What a budget doesn't buy you
The last thing to keep in mind is that a budget isn't about boundaries — it's about cost. So even if they look similar, they're not the same thing; they only mix because both are limits:
If you set up a $5 ceiling, it will be spent on a force-push or reading the secrets file into the log, but it won't stop any of these things. It's about waste, not damage
The blast radius comes from permissions and sandboxing — separate mechanisms with their own lessons
If you don't trust the agent with the repo, a budget isn't the right control for it
If you don't want to approve things, removing the approval steps doesn't cost less — they only remove the pauses, not the work
A run that hits the ceiling will produce half-finished changes by definition. And half-finished diffs are the most dangerous ones, as they're the easiest to be convinced (by yourself) that they're good
The budget is a narrow band-aid — it doesn't make things better, it just makes it harder for them to become worse. But even so, it's worth it — whatever breaks in the middle of the night, will show up in the morning via a log and a failed pipeline, at a cost you've pre-defined
scripts/capped-run.sh#!/usr/bin/env bash
# One unattended agent run, bounded three ways:
#
# 1. the provider's spend limit — set in the console, not here, and the only
# one of the three that still holds when this script is wrong
# 2. the harness's own cap — the flag on the line marked below
# 3. a watchdog this script owns — because the agent can't be trusted to
# enforce its own deadline
#
# WALL_CLOCK=900 BUDGET_USD=2 scripts/capped-run.sh "fix the failing parser test"
set -uo pipefail
PROMPT=${1:?give it a prompt}
WALL_CLOCK=${WALL_CLOCK:-900}
BUDGET_USD=${BUDGET_USD:-2}
LOG=${LOG:-run-$(date +%Y%m%d-%H%M%S).json}
# --- the harness cap. Swap this block for your tool. -------------------------
# codex exec no budget flag: the watchdog is the whole per-run cap
# copilot -p --max-ai-credits <n>, minimum 30
# agy -p --print-timeout <duration>
# agent -p no budget flag: the watchdog is the whole per-run cap
# kimi -p max_steps_per_turn in ~/.kimi-code/config.toml
claude -p "$PROMPT" \
--max-budget-usd "$BUDGET_USD" \
--output-format json >"$LOG" 2>"$LOG.err" &
run=$!
# --- the kill switch ---------------------------------------------------------
( sleep "$WALL_CLOCK"; kill -TERM "$run" 2>/dev/null ) &
watchdog=$!
wait "$run"; rc=$?
kill "$watchdog" 2>/dev/null
# --- what it cost, and why it stopped ----------------------------------------
if [ -s "$LOG" ] && command -v jq >/dev/null; then
jq -r '"$\(.total_cost_usd // 0) — \(.subtype // "no result") (\(.num_turns // 0) turns)"' "$LOG"
else
echo "killed at ${WALL_CLOCK}s with no result written — see $LOG.err"
fi
# 0 when it finished on its own, 1 when the budget tripped, and 143 (128 +
# SIGTERM) when the watchdog fired — unless the harness traps the signal and
# picks its own code. Treat anything non-zero as a failure.
exit "$rc"