Interactive Claude Code stops before a risky tool call — running a shell command, writing a file, pushing to a branch — and waits for a human to approve or deny it. That prompt is the safety mechanism. Point the same invocation at a CI runner with no terminal attached, and the prompt still fires; there's just nothing on the other end to answer it, so the job sits until the runner's timeout kills it. Headless mode doesn't remove that decision — it just moves it from "answered live, once, by whoever's watching" to "decided in advance, in a config file, before the job ever starts." Skipping that step, rather than making the decision explicitly, is where headless CI runs actually go wrong.
The non-interactive entry point (claude -p "prompt", print mode) makes Claude Code run to completion and exit instead of dropping into a REPL — that's the part that makes it usable from a pipeline step at all. It says nothing about permissions on its own. Pair it with no permission configuration and every tool call still tries to prompt; pair it with --dangerously-skip-permissions and every tool call is silently allowed, including ones you didn't anticipate. Neither is a policy — one hangs, the other has no floor. The actual policy lives in --allowedTools on the invocation or a permissions.allow / permissions.deny list in settings.json, checked into the repo the pipeline runs against.
Skip-permissions is tempting because it's one flag and the job goes green immediately. The cost shows up later: a pipeline with real deploy credentials, a database connection string, or push access to the default branch in its environment now has an agent that can use every one of them without anyone having decided it should. This isn't hypothetical caution — it's the same shape as running a build script you haven't read with full production environment variables in scope. The fix isn't "trust it less," it's "give it less to reach for": scope the credentials the job has to only what the task needs, and scope the tool allowlist the same way, so the two limits back each other up instead of one silently depending on the other.
| Tool call | Interactive default | CI default |
|---|---|---|
| Read files in the repo | Prompted once, then remembered for the session | Allow — read-only, no residual effect |
| Run the test suite, linter, type checker | Prompted | Allow — the reason the job exists |
Run arbitrary shell (bash -c "$anything") | Prompted per novel command | Deny by default; allow specific known commands by name, not a wildcard |
| Edit files in the working tree | Prompted | Allow if the job's output is a diff/PR, not a direct commit |
| Push, merge, or tag in git | Prompted | Deny — route through a PR a human merges, unless the pipeline's whole job is automated release |
| Network calls to arbitrary hosts | Prompted | Deny by default; allowlist the specific registries/APIs the job needs |
The pattern underneath the table: allow what's read-only or scoped to the job's own artifact, deny what has an effect outside the job (a push, a deploy, an arbitrary outbound request) unless that effect is the job. A pipeline that opens PRs for review is a very different trust boundary than one that pushes straight to production, even if both start from the same claude -p invocation.
An unattended run has no one to notice it's stuck re-trying the same failing command or burning turns on a task that should have stopped. Cap it the same way you'd cap any unattended job — a wall-clock timeout on the CI step itself, and, if the harness supports it, a turn or token ceiling on the invocation — so a stuck run fails the job instead of running until the runner's own timeout finally kills it hours later. Check the exit code in the pipeline step the same way you would for any other CLI tool; a non-zero exit is the signal to fail the build, not to retry blindly.
This is a narrower case of the same question as hook or skill: automation you actually depend on needs a guarantee, not a hope, and "hope the prompt gets answered" is not a guarantee once there's no one there to answer it. It also composes with git worktrees for parallel agents if the pipeline runs more than one headless job against the same repo concurrently — the isolation problem doesn't go away just because both agents are now unattended. What comes out of a CI run still needs a human check before merge — see reviewing an agent's diff for what a green pipeline won't catch. A headless job also inherits whatever's in its environment, which raises the same scoping question for data as for actions — see secrets and credentials in an agent's shell. And whatever tools the pipeline is permitted to call still need to survive a retry safely — see designing tools for agents to call.