# 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.

> **The default is not what you remember**
New terminal and VS Code sessions start in auto mode, but `claude -p` and the Agent SDK default to `default` mode. Be explicit about the mode in every script rather than inheriting it.


**Question:** Which pairing do the docs recommend for an unattended script?

- bypassPermissions on your laptop
- dontAsk plus --allowedTools
- plan mode
- default mode with no flags

**Answer:** dontAsk plus --allowedTools. dontAsk plus an explicit --allowedTools list means listed actions run and everything else is denied, with no prompt to wait on.


## 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-turns` caps turns.
- `--max-budget-usd` caps spend, using a client-side estimate. Treat it as a guard, not an invoice.
- `--permission-prompts none` denies anything that would have prompted and tells Claude not to retry.

```terminal
$ 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.json` are never auto-approved for writes, except in bypass.
- Removing critical targets, such as the filesystem root, your home or the working directory, with `rm` can never be approved by allow rules or hooks in any mode.
- `permissions.blockWritesOutsideWorkingDirectories` keeps writes inside the project.

> **Plan mode is not a fence under bypass**
In an interactive terminal where bypass is available, plan mode's edit block is not enforced: Claude is told not to edit, but edits would run. Do not treat a mode name as a guarantee when bypass is on the table.


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.

> **Rehearse a refusal**
Run a `-p` command with `--permission-mode dontAsk` and an allowlist that lacks something the task needs. Read the result's `permission_denials`, then decide whether the missing rule should be added or the task should not be unattended at all.


**Question:** Why put "never push" in a deny rule instead of telling Claude in the prompt?

- Prompts cannot contain the word push
- A deny rule applies in every mode and is enforced, while a spoken boundary is judgment and can be dropped by compaction
- Deny rules are faster
- The prompt is ignored in print mode

**Answer:** A deny rule applies in every mode and is enforced, while a spoken boundary is judgment and can be dropped by compaction. Deny rules apply in every mode including bypass. A boundary stated in chat is not stored as a rule and compaction can drop it.
