PricingSign inDownload

Blog / claude code

Put a limit on an unattended Claude Code run

Cap a scheduled or scripted Claude Code run by turns, spend and wall-clock time, and clean up the commands it leaves behind when it stops.

On this page
  1. Limit the number of turns
  2. Limit the spend
  3. Limit the time
  4. What none of these clean up
  5. Which limit to use
  6. Where Joxo fits
  7. Frequently asked questions
  8. How do I limit how long Claude Code runs?
  9. What does "Error: Reached max turns" mean?
  10. Does --max-budget-usd work with a Claude subscription?
  11. Why is a command still running after Claude Code stopped?
  12. Try it with your team

An agent that runs while you sleep has nobody to tell it to stop. A nightly job that gets stuck retrying a flaky test, or a loop over fifty repos where one of them sends the agent down a rabbit hole, can run for hours before anyone looks.

claude -p has three limits you can put on a run: how many turns it takes, how much it spends, and (with a small helper) how long it may take on the clock. This post shows each one, what you get back when it hits, and the one thing none of them clean up. Everything was tested on Claude Code 2.1.316.

Limit the number of turns#

A turn is one round of Claude working: it thinks, calls a tool, reads the result. --max-turns stops the run after that many:

bash
claude -p "Fix the failing test in test/api.test.js" --max-turns 20

When it hits the limit, the run ends with exit code 1 and prints:

Error: Reached max turns (20)

With --output-format json you get a result you can check in a script:

json
{ "subtype": "error_max_turns", "is_error": true, "num_turns": 21, "result": null }

Two things we saw: num_turns came back one higher than the limit (we set 3, got 4), and result is empty, so whatever Claude had done so far isn't summarised for you. Look at git status or the files to see where it stopped.

Pick the number from a normal run. Run the job once by hand with --output-format json, look at num_turns, and set the limit to two or three times that.

Limit the spend#

--max-budget-usd stops the run once its estimated cost goes over the amount you give it:

bash
claude -p "Summarise every open to-do in this repo" --max-budget-usd 0.50

At the limit the JSON result reads:

json
{ "subtype": "error_max_budget_usd", "is_error": true, "num_turns": 3 }

It's checked between turns, not in the middle of one, so a run can go a little past the amount. Ours went about 90% over a tiny test cap, a few tenths of a cent. On a normal-sized cap, the overshoot is one turn's worth. It also counted for us on a subscription sign-in, not only with an API key, so it works as a size limit either way.

Limit the time#

Claude Code has no flag for total run time. Use timeout (built into Linux; on a Mac, brew install coreutils gives you the same command):

bash
timeout 15m claude -p "Run the nightly dependency check and open an issue for anything outdated"
echo "exit code: $?"

When time runs out, timeout stops Claude and exits with code 124, so a script can tell a timeout apart from a normal failure.

What none of these clean up#

Here is the catch. We gave Claude a five-second timeout and asked it to run sleep 30. Claude stopped on time with exit code 124. One second later:

bash
pgrep -fl "sleep 30"
31188 sleep 30

The command Claude started was still running. Claude Code starts its shell commands in their own process group, so stopping Claude doesn't stop them. With sleep that doesn't matter. With a dev server, a test watcher or a database container, it means ports stay taken and the next night's run starts on top of last night's leftovers.

So after any capped run, clean up what it started. For a job that runs in its own folder, this catches most of it:

bash
#!/bin/sh
# run-capped.sh: run Claude with limits, then stop anything it left running
cd "${1:?usage: run-capped.sh <project folder> <prompt>}" || exit 1
timeout 15m claude -p "${2:?prompt missing}" --max-turns 40 --max-budget-usd 2 --output-format json > last-run.json
status=$?

# stop processes that still have this folder as their working directory
for pid in $(lsof -t +d "$PWD" 2>/dev/null | sort -u); do
  [ "$pid" != "$$" ] && kill "$pid" 2>/dev/null
done
exit "$status"

Check the list before you add kill to a job you run on your own machine: lsof -t +d "$PWD" names every process using the folder, which includes an editor you have open on it. On a build server or in a container, that's usually only the job. Our post on dev servers an agent leaves running has more ways to find them.

Which limit to use#

Use all three on anything unattended. They catch different failures:

  • Turns catch a loop: the agent retries the same fix over and over.
  • Spend catches a run that reads far more than it should, such as a whole monorepo or a huge log.
  • Time catches a hang: a command waiting on a login prompt or a network call that never answers. Turns and spend don't move while Claude waits on a tool.

Then read the result. A script should treat is_error: true or exit code 124 as "look at this in the morning", not as success.

Where Joxo fits#

When an unattended run stops on a limit, someone should know before the next one starts. With Joxo your agent's finished or stuck runs reach your phone, and your teammates see which of your sessions stopped and why. Setup is one line pasted into your agent.

Frequently asked questions#

How do I limit how long Claude Code runs?#

There's no time flag. Wrap the command in timeout, for example timeout 15m claude -p "…". It exits with code 124 when time runs out.

What does "Error: Reached max turns" mean?#

The run hit the --max-turns limit you set. Claude stopped where it was, with no summary. Check the files or git status to see what it finished.

Does --max-budget-usd work with a Claude subscription?#

In our test it stopped the run on a subscription sign-in too. It works on estimated cost and is checked between turns, so a run can go slightly over.

Why is a command still running after Claude Code stopped?#

Claude Code runs shell commands in their own process group, so stopping Claude, by timeout or by hand, doesn't stop them. Find them with lsof +d <folder> or pgrep -fl <command>.