Hardening Autonomous Runs
- The three risks that are unique to a run with no human watching
- An eight-item hardening checklist, in the order you should apply it
- Which permission primitive actually removes capability — and which only pre-approves it
- Why a least-privilege token matters more than a careful prompt
- How to cap a runaway loop with a platform-enforced budget instead of a hopeful instruction
- Why persistent instruction files need to be diffed to a place the agent cannot write
Running Claude headless or on a schedule — in CI, a cron job, a pre-commit hook — removes the human who'd normally catch a bad action. That convenience is exactly why these runs need the tightest guardrails.
The risks unique to unattended runs
- No one to say "no" to a risky tool call in the moment.
- Ambient credentials. CI often has powerful tokens (deploy, package registry, cloud). An agent there inherits them.
- Untrusted inputs. A run triggered by a PR or an issue may process attacker-authored content (injection).
The failure mode to design against is not a malicious model. It is an ordinary model, holding a token it should never have been given, acting on text an attacker wrote. Every item below removes one of those three ingredients.
A hardening checklist
- Block reading
.env, key files, and credential paths via permission deny rules. Don't rely on the model to avoid them. Deny rules are the only primitive that removes capability — see the six-step evaluation order for why an allow list cannot do this job. - Reserve "skip all prompts" for disposable sandboxes. And know what it does not override: deny rules, ask rules, and
PreToolUsehooks all run upstream of the mode. - Give the run a least-privilege token (read-only where possible), not your full-access credentials. This is the single highest-leverage item on the list, because it is the one an injected instruction cannot argue its way around.
- Run in a container that's destroyed afterwards; no persistent access to production. Pair it with credential masking so a leaked value is useless outside the run.
- Permit your test/lint/build commands; deny networked or destructive ones. Scope the rules —
Bash(npm test *), not a bareBash. - Max iterations, time budget, token/cost budget — so a loop or a manipulated agent can't run away. On the Anthropic-hosted loop, use Managed Agents Session Budgets for a hard, platform-enforced USD cap that pauses the session with
stop_reason: "budget_reached". - Prefer "open a PR / post a comment" over "push to main." A human merges. This converts every failure from an incident into a rejected diff.
CLAUDE.md,.cursorrules,SOUL.md,MEMORY.md, agent-skill files — anything auto-injected as instructions — should be diffed and logged to a place the agent cannot write to. See Mind Viruses for the measured spread mechanism this defends against.
Deny removes, allow only pre-approves
The most common mistake in a hardened run is reaching for the wrong lever. An allow list looks like a lock and isn't: anything you leave off it is still available to the agent and merely falls through to the session's permission mode.
Removes the capability — the tool is gone from the request
claude -p "run the test suite" \ --disallowedTools "Bash(curl *)" "Read(//.ssh/**)" "Edit(//etc/**)"
Makes a pre-approval list exact — anything else is denied, not prompted
claude -p "run the test suite" \ --permission-mode dontAsk \ --allowedTools "Bash(npm test)" "Read"
The second form is the one you want for CI: dontAsk turns every would-be prompt into a denial, so a run that encounters something unanticipated fails loudly instead of hanging or silently falling back on a permissive default.
A check that runs on every call
Allow and deny rules match patterns. When you need a check that inspects the content of a call — a secret-shaped string in an argument, an unexpected hostname — put it in a PreToolUse hook. Hooks run before every other permission step, and a hook denial holds even in bypass mode.
Refuse any tool call carrying secret material
# .claude/settings.json -> hooks.PreToolUse
# Exit code 2 blocks the call and tells Claude why.
jq -e '.tool_input | tostring | test("AKIA|BEGIN PRIVATE KEY|ghp_")' >/dev/null \
&& { echo "secret material in tool input" >&2; exit 2; } || exit 0Example: a safe CI reviewer
A PR-review bot should: check out the code read-only, have no deploy/secret access, run in a container, and comment its findings — never modify protected branches. See the PR-review walkthrough.
Check yourself
0/3- Unattended runs lose the human veto, inherit ambient credentials, and often read attacker-authored text — remove one ingredient at a time.
- Deny rules and hooks remove capability; allow lists only pre-approve. Pair allowedTools with dontAsk to make it exact.
- Scope the token before you tune the prompt. It is the defense an injection cannot talk past.
- Cap cost and iterations with a platform-enforced budget, not an instruction.
- Ship outputs as a PR or a comment so a human merge stays in the loop.
- Diff auto-injected instruction files to storage the agent cannot write.
Next
- Agent SDK Permissions: the Six-Step Order — exactly which primitive wins, and the five ways an allowlist silently does nothing
- Permissions & Permission Modes
- Securing Agents & Tools
- Headless Mode & the Agent SDK
- The claude CLI: Flags That Change Outcomes — the bounded-run flag set, and
--restrictedfor a shared machine