Running with nobody watching
When no one can answer a prompt, the permission rules are the only supervisor left. The safe setup is narrow allow rules, hard deny rules, a capped run and nothing valuable within reach.
What changes when you leave
Interactive Claude Code asks you. Every approval prompt is a small check that a person looked at the action. Run it unattended and that check disappears, so something else has to do the job. There are four layers: the permission mode, deny rules, limits on the run, and what is within reach in the first place.
The order matters. A mode is a convenience. A deny rule is a boundary.
Choosing the mode
Six modes exist: default, acceptEdits, plan, auto, dontAsk and bypassPermissions. For unattended scripts the docs recommend dontAsk together with --allowedTools. The idea: whatever you listed is allowed, and anything else is denied rather than asked.
bypassPermissions belongs only in disposable containers. Bypass is refused when running as root or under sudo outside a recognised sandbox.
Auto mode is a middle path. A classifier model reviews each non-trivial action. It stops being a fit for long unattended work in one specific way: three consecutive classifier blocks, or twenty in total, pause auto mode and resume prompting. If nobody is there, the run stalls.
Which pairing do the docs recommend for an unattended script?
Deny rules are the real fence
Auto mode and a boundary you stated in chat ("don't push") are judgment, not enforcement. A chat boundary acts as a block signal but is not stored as a rule, so compaction can drop it. Put hard "never" rules in deny rules or hooks.
Deny rules apply in every mode, including bypass. Allow rules have no effect in bypass. That asymmetry is why deny is the safety layer.
Some examples from the best-practices page: Bash(git push *), Read(./.env), Read(./secrets/**). Know their reach, too. A Read deny also catches cat .env, but not a script that opens the file. A WebFetch domain deny does nothing about curl to that domain unless the sandbox is on.
That is where /sandbox comes in. It confines Claude at the OS level on macOS, Linux and WSL2 and picks up your Read deny rules and WebFetch domain rules. Strict sandbox mode removes the retry-outside-sandbox escape hatch.
Bound the run
A loop with no exit runs until the money is gone. In print mode:
--max-turnscaps turns.--max-budget-usdcaps spend, using a client-side estimate. Treat it as a guard, not an invoice.--permission-prompts nonedenies anything that would have prompted and tells Claude not to retry.
$ claude -p "fix the lint errors" --permission-mode dontAsk --allowedTools "Bash(npm run lint)" "Edit" --max-turns 15 --max-budget-usd 2✻ Runs lint, edits files✻ Tries to run git push✻ Denied, reported as permission_denied# The run continues without it.
In a -p run without a permission-prompt tool, repeated blocks drop the action and the run continues rather than stopping. So read permission_denials in the result. A quiet run is not proof that nothing was refused.
Keep things out of reach
The strongest limit is what Claude cannot get to. Several protections exist, and each has an edge.
- Protected paths such as
.git,.claude,.vscode,.idea, shell rc files and.mcp.jsonare never auto-approved for writes, except in bypass. - Removing critical targets, such as the filesystem root, your home or the working directory, with
rmcan never be approved by allow rules or hooks in any mode. permissions.blockWritesOutsideWorkingDirectorieskeeps writes inside the project.
And mind what a headless run loads. Without --bare, -p still runs project hooks and connects .mcp.json servers, even in a folder you never trusted. In CI use --bare, give it an API key, and give that key as little as the job needs.
Why put "never push" in a deny rule instead of telling Claude in the prompt?
Chapter complete
You've finished chapter 6 of 6 in Parallel and unattended.