# Unattended runs with limits

Course: Claude Code: Pro Level — https://zero2vibecode.com/learn/claude-code-pro
Canonical URL: https://zero2vibecode.com/learn/claude-code-pro/3-2-bounded-unattended-runs
Verified against: Claude Code v2.1.292 (2026-10-15)
Last updated: 2026-10-15

An unattended run does only what you allowed in advance. So allow little and limit the rest — minimal permissions, a strict sandbox, turn and budget caps, no commits, and a cost estimate on a small slice.

In the last lesson you ran Claude from a script. Now — a run that works **unattended**, for a long time, editing files. The core principle: a run like this does exactly what you allowed in advance. So allow little, and limit the rest.

## Five limits

1. **Minimal permissions.** The `dontAsk` mode denies anything that would have needed a question. Allow only the folders the task edits and the one command it runs.
2. **The sandbox.** Let the system limit what the allowed commands can do — and do not let the run start if the sandbox is unavailable.
3. **Caps.** `--max-turns` caps the turns, `--max-budget-usd` the approximate cost.
4. **A review gate.** Deny `git commit` and `git push`. The result stays in the working tree, and you commit — after reading the diff.
5. **A cost estimate.** First run a small slice of the task, and set the budget limit from that.

## The limits file

It is handy to keep the limits in a separate file, for example `.claude/bounded-run.json`:

```json
{
  "permissions": {
    "allow": ["Edit(./src/**)", "Edit(./test/**)", "Bash(npm test *)"],
    "deny": ["Agent", "Read(./.env)", "Read(./.env.*)", "Bash(git commit *)", "Bash(git push *)"]
  },
  "sandbox": {
    "enabled": true,
    "failIfUnavailable": true,
    "allowUnsandboxedCommands": false
  }
}
```

- a `deny` on `Agent` stops Claude from handing work to a subagent, which might have its own permission mode;
- `failIfUnavailable` — do not start without the sandbox;
- `allowUnsandboxedCommands: false` — do not retry a command outside the sandbox.

## The run

```bash
claude -p "Add a test for one uncovered function in src/links.js" \
  --setting-sources project \
  --settings .claude/bounded-run.json \
  --permission-mode dontAsk \
  --model sonnet \
  --max-turns 15 \
  --max-budget-usd 2 \
  --output-format json
```

`--setting-sources project` leaves out your personal settings — for example, a permission saved with "Yes, and don't ask again". Only what you explicitly wrote into the limits gets in.

When a limit is hit, the exit code is non-zero, and `subtype` in the JSON explains why: `error_max_turns` or `error_max_budget_usd`. The `total_cost_usd` field is a client-side estimate, not your bill.

## You are the review gate

After the run, read `git status` and `git diff`, including the limits file itself, run the tests yourself, and commit only what you accept. For a second opinion, ask a fresh Claude session to review the diff in a clean context.

**Careful.** 
In dontAsk mode the protected .claude/ folder cannot be written to by Claude's tools — it cannot loosen its own limits. But you write the limits file by hand: reread it after every change as carefully as code. One extra asterisk in allow opens up more than it seems.

**Practice in the terminal.** First run the task with --max-turns 1 and look at the subtype: error_max_turns means the limit works. Then run the same task with --max-turns 15 and --max-budget-usd 2.

**Limits in a minute.** 
The [settings builder](https://zero2vibecode.com/claude-code-settings-builder?preset=bounded) puts together the limits for an unattended run: a strict sandbox, no commits or pushes, and a guard hook for .env.
