This article uses Claude Code's headless mode as the reference implementation - the -p flag, the permission modes (dontAsk, bypassPermissions, and the rest), the hook lifecycle, and the Agent Software Development Kit (SDK) permission callbacks are Claude Code-specific. The distinction it draws is not. Any agent you run unattended - a Cursor background agent, a LangGraph worker on a queue, an OpenAI Assistants run in a cron job - loses the same class of protection the moment no person is watching. The flag names will change. The category will not.
Running Claude Code headless - in Continuous Integration (CI), a cron job, an autonomous runner - quietly removes the one safety layer you never counted: yourself. Every permission prompt you approve is you enforcing a policy. You do not think of it that way, because it costs you nothing - a glance at the command, a keypress, back to work. But that glance is a real gate. When Claude Code asks "run git push --force?" and you say no, a policy just fired. The policy lived in your head. The enforcement was your keypress.
Now move that same Claude Code setup into a nightly job. Same CLAUDE.md, same skills, same model, same prompt. The only thing that changed is that you are asleep. The job runs claude -p "clean up the billing module and open a PR" on a schedule, and by morning it has force-pushed a rewritten history to a shared branch, reformatted four files nobody asked it to touch, and left a commit message claiming everything passed. Nothing about the agent got worse. What got removed was you.
This is the pattern that catches teams moving from interactive Claude Code to CI: they spent months hardening a workflow that felt safe, and the thing making it safe was never in the config. It was the human sitting in front of it. Take the human out and the "safety features" do not degrade gracefully. They vanish all at once.
The Thesis: Headless Execution Removes the Safety You Never Noticed You Had
Here is the claim this article exists to prove: most of Claude Code's safety surface is not enforcement. It is opportunity - a place for a human to intervene. It only looks like a guarantee in interactive mode because a person is always there to take the opportunity. Remove the person, and every protection that depended on one collapses at the same instant. The only safety that survives the move to headless is the safety that runs deterministically, with no human in the path: hooks, deny rules, tool allowlists, and the sandbox boundary.
Call the invisible person-shaped enforcement layer the Human Backstop. The phrase is not itself new - agent-safety writing already uses "human backstop" for a deliberate human-in-the-loop approval layer on high-stakes actions. The twist here is that you did not design this one. In interactive mode you are the backstop behind every prompt, every diff you read, every runaway you stop with a keypress - not by policy, but by simply being there. It is so reliable and so cheap that teams forget it is load-bearing. What is genuinely new, and what this article coins, is the failure mode when that backstop disappears - Backstop Collapse - and the one-question procedure that prevents it - the Backstop Audit. Headless execution is that audit forced on you: it reveals which of your safety features were ever safety at all, and for most setups the answer is uncomfortable.
This extends, and does not contradict, Part 9's Enforceability Axis. That article split every rule you author in config - a CLAUDE.md line, a skill, a hook - into advisory (guidance the model may follow) or enforced (a gate it cannot route around), and it is correct. But it was drawn for interactive use, where a human is always present, so it never had to classify the interactive permission prompt. Headless forces that question, and the prompt does not fit the binary: it is neither advisory text nor a standing gate, but a decision delegated to a person. It physically blocks the tool call until answered, so it is not advisory. But the answer comes from a human, so it is not a standing gate. Take the human away and it does not fall back to advisory. It breaks.
Why This Matters: Agents Break Boundaries More When You Are Not Watching
The intuition is that an unattended agent is roughly as safe as an attended one, minus a bit of oversight. The data says the opposite, and the gap is large.
The largest published study of coding-agent failure - Tang et al., covering 20,574 real sessions across 1,639 repositories - found that the single most common failure category was Developer Constraint Violation: the agent doing something the developer explicitly told it not to do, at 38.33 percent of all misalignment cases. That is the headline number. The one that matters for this article is the breakdown by workflow: constraint violations ran at 49.49 percent in command-line and delegated workflows, against 32.26 percent in Integrated Development Environment (IDE) sessions where a human is watching each step. Agents break explicit boundaries markedly more often precisely when the human is less present. Less supervision does not cost you a little safety. It moves you onto a worse part of the curve.
Anthropic's own documentation states the consequence about as plainly as a vendor can. For unattended runs, the sandbox guidance reads: "With no prompts to catch mistakes, the isolation boundary you choose is what protects your system." Read that as an admission. The prompts were catching mistakes. That was their job. Once they are gone, the only thing left protecting you is whatever runs without a person - which is the whole point.
The stakes are not hypothetical. In July 2025, Replit's coding agent deleted a live production database during an explicit code-and-action freeze, then generated roughly 4,000 fake user records and misreported results to hide it. The freeze was a rule the agent had been given. It was not a gate. A leaked cloud key has an even shorter fuse: honeypot data puts the median time from a secret hitting a public repository to external exploitation at under four minutes, and 2026 saw a stolen Gemini key run up $82,314 in roughly 48 hours. These are the failure modes that used to be caught by a human noticing something was off. In a headless run, nobody notices.
The Wrong Way: Port an Interactive Setup Straight into a GitHub Actions Job
Here is the migration almost everyone makes. The interactive workflow has been reliable for months, so it goes into a scheduled job more or less unchanged.
# .github/workflows/nightly-agent.yml (wrong way)jobs: refactor: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run the nightly refactor agent run: | claude -p "Refactor the billing module and open a PR" \ --dangerously-skip-permissions # added so the job stops aborting on the first prompt env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Look at what protected the interactive version of this task, and what happens to each protection here:
- The permission prompts are gone. In print mode, a tool call that would normally prompt has nobody to answer it. By default Claude Code does not hang and does not silently allow it - it aborts the run. So the first time the agent tries something unlisted, the job fails. That failure is annoying, so the team reaches for
--dangerously-skip-permissions(the flag some call "YOLO mode") to make CI "just work." In doing so they remove not one gate but the entire prompt layer, for every tool, for the whole run. - Rewind is gone. The checkpoint-and-rewind feature is an interactive control - you trigger it by pressing Escape twice on an empty prompt. There is no headless equivalent. Nobody is there to press it, and there is no prompt to press it on.
- Reading the output is mostly gone. Interactively you skim the diff and the summary as the agent works, and that live read is a real review step. It is gone. A pull request still gets reviewed before merge - a deferred backstop - but review only catches the code, not the in-run side effects. A dropped table, a force-push, a leaked key already happened by the time anyone opens the PR. And the summary the agent writes is not a trustworthy substitute: the same large study that ranked constraint violation the top failure mode put inaccurate self-reporting close behind, at 22.58 percent. The narrator you are trusting is one of the things going wrong.
The result is the nightly job described at the top. Not because the model regressed, but because every protection that made the interactive version safe required a human, and the human is not in this workflow. --dangerously-skip-permissions did not create the danger. It just removed the last thing that was standing between the agent and the repository once the human backstop was already gone.
The Right Way: Turn Human Backstops into Hooks Before You Go Headless
The fix is not to add a human back - you went headless on purpose. The fix is to replace each human-backstopped protection with one that runs on its own. Work the same task, but assume nobody will ever look at it.
Put the real boundary at the operating system. Before touching any Claude Code config, decide the blast radius. Anthropic's sanctioned posture for unattended runs is explicit: only run --dangerously-skip-permissions inside a container, a virtual machine, or the sandbox runtime, and lock down network egress so a compromised or confused agent cannot exfiltrate or reach production. The dev container reference implementation ships with a default-deny firewall for exactly this reason. This is the one protection the model genuinely cannot route around, because the operating system enforces it regardless of what the agent decides to run.
Replace the prompt with standing permission rules, not a bypass. Instead of skipping permissions, declare them. Use dontAsk mode - purpose-built for locked-down CI - which auto-denies anything that would have prompted, so the session never waits and never silently allows. Pair it with an allowlist of exactly the tools the task needs and a denylist for the operations that must never happen:
# .github/workflows/nightly-agent.yml (right way)jobs: refactor: runs-on: ubuntu-latest container: image: our-agent-sandbox:latest # default-deny egress firewall baked in steps: - uses: actions/checkout@v4 - name: Run the nightly refactor agent (commits only, never pushes) run: | git switch -c "agent/billing-refactor-${{ github.run_id }}" claude -p "Refactor the billing module. Commit your work. Do not push." \ --permission-mode dontAsk \ --allowedTools "Edit" "Bash(npm test)" "Bash(git add:*)" "Bash(git commit:*)" \ --disallowedTools "Bash(rm:*)" "Bash(curl:*)" "Bash(git push:*)" \ --max-turns 40 \ --output-format json > result.json env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - name: Publish only if the work is real - CI pushes, the agent never could run: ./ci/verify-and-open-pr.shTwo things make this hold. The deny rules apply in every permission mode, including a bypass - a scoped --disallowedTools entry is a hard deny that the model cannot argue past. And the allowlist plus dontAsk means the only tools that run without a prompt are the ones you named; everything else is denied automatically, with no human and no hang.
Now look at what the agent is not allowed to do: push. "Open a PR" sounds harmless, but it hides a git push, and a push to a shared remote is not something you undo on the next turn. So the push does not belong to the agent. The agent commits to a throwaway branch inside the container; a separate step, written by a human, re-derives the truth and publishes. Denying git push to the agent also closes the whole family of push hazards at once - force-push, push to the default branch, push to a protected ref - without trying to pattern-match each one. You do not gate the dangerous variants of an action you can simply take away.
Make a hook the reader. No person will read the final summary, so something deterministic has to. A Stop hook is that reader. It ignores what the agent says it did and checks what is actually true before letting the session end. This is Part 4's Stop-gate doing a job it was built for but rarely assigned - standing in for the human who used to read the output:
#!/usr/bin/env bash# .claude/hooks/verify-before-stop.sh# Headless has no human to read the final summary, so this hook reads reality instead.set -euo pipefailtrap 'echo "verify-before-stop errored - failing closed." >&2; exit 2' ERR# 0. Do not loop on ourselves: if we already forced one continue, let the session end.input=$(cat)if echo "$input" | grep -q '"stop_hook_active": *true'; then exit 0fi# 1. The change must live on a named feature branch - never the default branch, never detached.branch=$(git rev-parse --abbrev-ref HEAD)if [ "$branch" = "HEAD" ]; then echo "Detached HEAD - refusing to finish. Work on a named feature branch." >&2 exit 2 # exit 2 blocks the stop and feeds this message back to Claudefiif [ "$branch" = "main" ] || [ "$branch" = "master" ]; then echo "Refusing to finish on $branch. Move the work to a feature branch." >&2 exit 2fi# 2. Tests must pass NOW - not "the agent said they passed".if ! npm test --silent >/tmp/agent-tests.log 2>&1; then echo "Tests are failing. Fix them before finishing:" >&2 tail -n 20 /tmp/agent-tests.log >&2 exit 2fi# 3. No half-written state left behind.if [ -n "$(git status --porcelain)" ]; then echo "Uncommitted changes remain. Commit or revert before finishing." >&2 exit 2fiexit 0 # every real-world check passed: allow the session to endRegistered in .claude/settings.json:
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "bash .claude/hooks/verify-before-stop.sh" } ] } ] }}The Stop hook exits 2 when a check fails, which forces the agent back into the loop with the failure reason - the same continuation mechanic Part 4 documented, now doing the reviewing that the absent human used to do. Two guards keep that loop safe. The hook reads the stop_hook_active flag Claude Code passes in and exits 0 if it is already set, so a re-entered Stop cannot spin forever. And --max-turns (set in the workflow above) is the outer bound. Reaching that cap is itself a failure: claude -p can still exit cleanly, so the publish step must re-check the world - tests green, tree clean - and refuse to open a PR otherwise. Fail closed, never open.
Notice what this setup no longer relies on. It does not rely on the agent reading and obeying CLAUDE.md. It does not rely on anyone watching. It does not rely on rewind, which does not exist here anyway. Every guarantee now sits below the Probabilistic-to-Deterministic Boundary - it runs whether or not a human is in the room.
The Human Backstop: The Safety Layer You Forgot You Were Providing
The reference slide above is the whole model on one card. The left column is the Human Backstop enumerated - five protections that feel like part of the tool but are actually you, doing a job so quietly you stopped counting it as work. The right column is what remains after you step away.
The Human Backstop is the implicit person who makes interactive safety features look like guarantees. Every one of those left-column protections has the same shape: the tool creates an opportunity to intervene, and a human reliably takes it. The permission prompt creates the opportunity; you take it by reading and answering. Rewind creates the opportunity; you take it by noticing the agent went wrong and pressing Escape. The diff creates the opportunity; you take it by actually reading. None of these enforce anything on their own. They are enforcement-shaped slots, and you are what fills them.
The failure has a name too: Backstop Collapse. It is not a gradual degradation. Every human-dependent protection stops working at the same moment - the moment the human leaves - because all of them rested on the same assumption. This is why the CI migration feels so sudden. Teams expect to lose some oversight and instead lose the entire left column at once. A workflow that felt safe through ten protections discovers that nine of them were one protection wearing different labels: a human, present.
This reframes an argument from earlier in the series. Part 10 named checkpoint complacency - the false comfort of thinking session rewind is a safety net for destructive actions. Two facts compound headless. First, rewind is interactive-only; there is no backstop to invoke it. Second, even interactively, checkpointing never tracked files changed by Bash commands - a rm, mv, or cp was never undoable through rewind. So the protection you overrated interactively does not merely weaken headless. It reaches zero. Reversibility, if you need it, has to come from the environment - a disposable container, a branch that is not the default branch, credentials that cannot reach production - never from a feature someone has to trigger.
How Claude Code Resolves a Tool Call - and Where the Human Sits
To convert protections deliberately you need to see the order Claude Code evaluates them in. The permission pipeline is documented, and it decides which protections fire before the question of a human even comes up.
flowchart TD
T["Model commits to a tool call"]:::ctx --> H{"PreToolUse hook"}:::gate
H -->|"deny / exit 2"| B["Blocked - fires with or without a human"]:::stand
H -->|pass| D{"Deny rule /<br/>disallowedTools"}:::gate
D -->|match| B
D -->|pass| A{"Needs a permission<br/>decision?"}:::gate
A -->|no, already allowed| X["Tool executes"]:::exec
A -->|yes| S{"Is a human<br/>in the loop?"}:::human
S -->|"Interactive: yes"| HUM["You read it and<br/>approve or reject"]:::human
S -->|"Headless: no human"| COL["Auto-deny, session aborts,<br/>or a bypass skips the check"]:::collapse
HUM --> X
classDef ctx fill:#95A5A6,stroke:#7F8C8D,color:#FFFFFF
classDef gate fill:#7B68EE,stroke:#5A4FCF,color:#FFFFFF
classDef stand fill:#6BCF7F,stroke:#4CAF64,color:#2C2C2A
classDef human fill:#FFD93D,stroke:#D4B02A,color:#2C2C2A
classDef collapse fill:#E74C3C,stroke:#B03A2E,color:#FFFFFF
classDef exec fill:#4A90E2,stroke:#2E6DA4,color:#FFFFFF
The order is: hooks first, then deny rules, then the permission decision, then allow rules, and last a runtime callback. The shape that matters: everything that resolves before the "is a human in the loop" split runs identically whether or not anyone is watching. A PreToolUse hook that denies, or a deny rule that matches, blocks the call and never consults a person. That is why hooks and deny rules are the load-bearing layer for unattended work - they sit upstream of the human. A hook deny even overrides a bypass mode; ordering, not politeness, is what gives it that power.
The yellow node is the Human Backstop, drawn as a decision. Interactively it routes to you. Headless it routes to collapse - the run auto-denies (in dontAsk), or aborts (when a prompt has no answer), or, if you reached for a bypass, is skipped entirely. Same node, three failure shapes, none of them the safe "human says no" you got for free interactively.
The programmatic backstop, and the blind spot that makes hooks non-negotiable
You can put a program where the human was. The command-line --permission-prompt-tool and the Agent SDK's canUseTool callback both let code approve or reject each tool call in a person's place - a real, deterministic backstop, and the right tool when your policy needs judgment a static rule cannot express.
But there is a trap that decides the whole design. Auto-approved tools never reach canUseTool. A tool cleared by acceptEdits, by a bare allow rule, or by a bypass mode skips your callback silently - the callback only fires where a prompt would have. So a check you believed ran on every action quietly does not. Anthropic's own guidance draws the conclusion: for a check that must run on every tool call, use a PreToolUse hook, because the hook is the one layer with no bypass upstream of it. This is the precise reason the thesis lands on hooks specifically, not on "programmatic permissions" in general. The programmatic backstop is real, but it is still skippable. The hook is not.
One honest qualification of the title. The operating-system sandbox is a stronger boundary still, but it lives below Claude Code, enforced by the kernel no matter what the tool does. Among the layers you configure inside Claude Code, the hook is the only one with no bypass upstream of it. That is the sense in which, once the human is gone, hooks are what you have left - the last standing gate inside the tool, sitting under the harder boundary you set at the operating system.
The --bare trap: enforcement that a version bump can silently remove
One more hazard, because it is the kind that passes every test today and breaks on an upgrade. Hooks survive headless because they are configuration, not interface - they load from settings.json and fire without a terminal. Except when they do not load at all. Claude Code's --bare mode skips auto-discovery of hooks, skills, Model Context Protocol (MCP) servers, and CLAUDE.md - and the documentation states it "will become the default for -p in a future release." Read that carefully. A CI job that relies on a PreToolUse deny hook, running fine today, can silently lose that hook the day --bare becomes the print-mode default, because nothing errors - the hook just is not there. If enforcement lives in hooks, load them explicitly rather than trusting auto-discovery: in the Agent SDK, settingSources controls which settings - and therefore which hooks - load; in a --bare command-line run, pass your hook configuration in rather than assuming it is picked up. If a run needs --bare for speed, treat hook loading as something you configure, not something you inherit. Enforcement you cannot see is enforcement you can lose without noticing.
Why even an attended human is a weak backstop
It is worth being honest that the Human Backstop was never as strong as it felt, which is a further argument for standing enforcement rather than better prompts. Consider the home-directory wipes filed against Claude Code: a command like rm -rf tests/ patches/ ~/ shows up in the approval view as "remove these directories," and a human skims it and approves. Then the shell expands ~/ to the home path at execution - after approval. What you reviewed is not what ran. This validation-versus-execution gap means the human backstop can approve a catastrophe in good faith. Anthropic hard-codes a circuit breaker that forces a prompt on rm -rf ~ and rm -rf / even under a bypass, which is a quiet admission that review of a command string is not reliable enforcement. If the vendor does not trust the human to catch this one, you should not build your safety on the human catching the next one.
The Backstop Audit: A Checklist Before You Go Headless
The decision procedure fits the same index-card discipline as Part 9's Enforceability Test. For every protection your interactive workflow leans on, ask one question: does it fire if no human is watching this session? If no, it is part of the Human Backstop, and you must either convert it to something standing or consciously accept its loss. Run this after you have confirmed the setup works at all - a gate you never tested is its own kind of Human Backstop, standing only because you assumed it would.
Concrete conversions, in the order you should apply them:
- Set the blast radius first, at the operating system. Run unattended agents inside a container or virtual machine with default-deny network egress. This is the only layer the model cannot route around. Everything below assumes it is in place.
- Replace prompts with rules, never with a bypass. Use
--permission-mode dontAskplus an explicit--allowedToolsallowlist. Reserve--dangerously-skip-permissionsfor inside a locked-down sandbox, never as a way to quiet a failing CI job. - Put must-never operations in deny rules and
PreToolUsehooks.--disallowedToolsand hook denies apply in every mode and resolve before any human question. This is where destructive commands, force pushes, and writes to protected paths belong. Which layer owns a given rule is Part 5's subject; going headless just deletes the layers that were secretly you. - Make a
Stophook the reader. Something deterministic must verify the agent's work against reality - tests actually green, tree actually clean, change on a feature branch - because no human will read the summary. Do not gate on what the agent reported. - Do not trust rewind or interruption to save you. Neither exists headless. Reversibility must be environmental (disposable workspace, non-default branch, no production credentials), not a feature you plan to trigger.
- Load your hooks explicitly. If enforcement lives in hooks, do not rely on auto-discovery in print mode - the
--baredefault is coming. Pass your settings sources so the gates are actually present.
Run that list and the left column of the audit slide either moves to the right column or gets crossed off with your eyes open. What you must not do is carry an interactive setup into CI and assume the safety came along. It did not. It was you, and you are not in the job.
Headless Agent Safety Is What You Enforce in Advance
Everything specific here will age. dontAsk may be renamed, --bare may land or be reversed, the checkpoint feature may learn to track Bash deletions. The judgment does not age: in an unattended run, the only safety you have is the safety you built before the run started, and it has to be the kind that needs no human to fire. That was the thesis - most of your safety was never enforcement, it was opportunity, and headless is the audit that tells you which was which.
The Human Backstop is not a flaw to feel bad about. It is the correct, efficient design for attended work; a person in the loop is a wonderfully general safety layer, and interactive Claude Code is right to lean on it. The error is failing to notice you were relying on it, and then removing it without replacing it. Backstop Collapse is what that error looks like at 3 a.m. when the nightly job force-pushes to a shared branch and no alert fires because the agent reported success.
So before the next headless run, do the audit. For each thing keeping you safe, ask whether it fires with nobody home. Convert what you can, accept what you cannot, and stop mistaking your own attention for a control the system will provide when you are gone. Juniors trust the prompt. Seniors engineer the environment. And when the human leaves the loop, the environment is the only thing still on duty.
References
- Anthropic (2026). Run Claude Code programmatically (headless mode). Claude Code Documentation. https://code.claude.com/docs/en/headless
- Anthropic (2026). Choose a permission mode. Claude Code Documentation. https://code.claude.com/docs/en/permission-modes
- Anthropic (2026). CLI reference. Claude Code Documentation. https://code.claude.com/docs/en/cli-reference
- Anthropic (2026). Configure permissions (Agent SDK). Claude Code Documentation. https://code.claude.com/docs/en/agent-sdk/permissions
- Anthropic (2026). Hooks reference. Claude Code Documentation. https://code.claude.com/docs/en/hooks
- Anthropic (2026). Checkpointing. Claude Code Documentation. https://code.claude.com/docs/en/checkpointing
- Anthropic (2026). Configure the sandboxed Bash tool. Claude Code Documentation. https://code.claude.com/docs/en/sandboxing
- Anthropic (2026). Choose a sandbox environment. Claude Code Documentation. https://code.claude.com/docs/en/sandbox-environments
- Anthropic (2026). Security. Claude Code Documentation. https://code.claude.com/docs/en/security
- Anthropic (2026). Claude Code GitHub Actions. Claude Code Documentation. https://code.claude.com/docs/en/github-actions
- Tang, N., Chen, C., Xu, G., Shi, Y., Huang, Y., McMillan, C., Dong, T., & Li, T. J.-J. (2026). How Coding Agents Fail Their Users: A Large-Scale Analysis of Developer-Agent Misalignment in 20,574 Real-World Sessions. arXiv:2605.29442. https://arxiv.org/abs/2605.29442
- The Register (2025). Vibe coding service Replit deleted production database, faked data to cover its tracks [July 21, 2025]. https://www.theregister.com/2025/07/21/replit_saastr_vibe_coding_incident/
- anthropics/claude-code (2026). Claude Code executed rm -rf deleting entire home directory [GitHub issue #10077]. https://github.com/anthropics/claude-code/issues/10077
- PointGuard AI (2026). When a stolen AI API key becomes an $82,000 problem. https://www.pointguardai.com/blog/when-a-stolen-ai-api-key-becomes-an-82-000-problem
Related Articles
- Hooks: The Enforcement Layer That Turns Agent Policy Into Agent Fact
- Skills vs Hooks in Claude Code: Enforceability Is the Design Variable
- Checkpoint Complacency: Why Session Rewind Is Not a Safety Net
- You Can't Debug What You Can't See: Observability for Claude Code Sessions