Headless Mode: Running CLI Agents Non-Interactively
A headless cli agent isn't the interactive agent minus a screen — it's a different discipline. Here's how to replace every human guardrail with an explicit configured one so unattended runs don't hang or spiral.
# Wrong: interactive habits, no UI, hope for the best claude -p "clean up the codebase and fix any issues you find"
Most headless-mode guides are wrong about what headless mode actually is. They present it as "the interactive agent, minus the terminal UI" — same tool, fewer keystrokes. That framing quietly causes half the failures people hit, because a headless cli agent isn't interactive-minus-a-screen. It's a fundamentally different operating mode with different failure modes, different safety requirements, and different success criteria. Treat it as the same tool without a UI and you'll build automation that hangs, spirals, or silently does the wrong thing at 3 a.m.
Let's tear down the "interactive minus UI" mental model and replace it with one that actually holds up unattended.
Before: The "Interactive Minus UI" Setup
The weak setup takes an interactive workflow and just removes the human, assuming everything else transfers.
[object Object],
claude -p ,[object Object],What this does: Runs a one-shot pass with a vague, open-ended instruction — the kind you'd give interactively and then steer as it went. But there's no steering here. "Fix any issues you find" with no human to course-correct is an invitation to an unbounded, unpredictable run. The instruction that's fine interactively is dangerous headless.
The problem is that interactive mode has an invisible safety feature: you. When the agent goes sideways interactively, you stop it, redirect it, answer its question, approve or deny its command. Strip the human out and every one of those implicit safety mechanisms vanishes — but the weak setup doesn't replace them with anything.
⚠️ Common mistake: Porting an interactive prompt straight to headless mode. Interactive prompts assume a human will refine the goal, answer clarifying questions, and catch mistakes mid-run. A headless cli agent gets none of that, so its instructions must be complete, bounded, and unambiguous up front — because there's no second chance to clarify.
Why It Fails: No Human Means No Implicit Guardrails
Headless mode fails when it inherits interactive assumptions, and there are three specific ones that bite.
The first is the clarifying question. Interactively, an agent that's unsure asks you. Headless, it can't — so it guesses, and a guess on an ambiguous instruction is where wrong-but-plausible output comes from. The instruction has to be complete because there's no dialogue to complete it.
The second is the permission prompt. Interactively, a risky command stops and waits for your yes. Headless, if you haven't pre-configured what's allowed, the run either hangs forever waiting for an answer that never comes, or — worse — runs with permissions off and does whatever it likes.
The third is the resume dialog. Some agents, on startup, show a prompt asking whether to resume a previous session. Interactively you just answer it. Headless, that dialog is a trap: the job blocks on a question no one will answer, and your overnight run does nothing but wait until it times out.
After: A Proper Headless CLI Agent Run
The strong setup replaces every removed human guardrail with an explicit configured one.
First, make the instruction complete and bounded — a specific task, not an open-ended goal.
claude -p ,[object Object], \
--permission-mode dontAsk \
--allowedTools ,[object Object], ,[object Object], ,[object Object], \
--max-turns 6What this does: Gives one specific, bounded task with an explicit success criterion (tests pass) and an explicit boundary (change nothing else), pre-approves exactly the tools the task needs, and caps the run at six turns. Every guardrail a human would have provided interactively is now configured explicitly.
Second, capture output as structured data so the run is observable and checkable by a script.
claude -p ,[object Object], --output-format json > result.json
jq ,[object Object], result.jsonWhat this does: Emits the run's cost, turn count, and error status as JSON so a downstream script can decide whether the run succeeded, log its cost, and alert on anomalies. Headless runs need programmatic success checks because there's no human watching the output scroll by.
Third, check the exit code and gate on it. A headless run is a program; treat its exit status like one.
[object Object], claude -p ,[object Object], --output-format json > out.json; ,[object Object],
,[object Object], ,[object Object],; ,[object Object], ,[object Object], ,[object Object],; ,[object Object],What this does: Uses the agent's exit status to decide the next pipeline step, so a failed run stops the automation instead of letting it commit garbage. This is the headless equivalent of a human noticing something went wrong.
⚡ Pro tip: Neutralize the resume-dialog trap explicitly. If your agent shows a resume prompt on startup, either configure it off or run in a clean environment with no prior session to resume. An overnight job that hangs on an unanswered resume dialog is the single most common "why did nothing happen" failure in headless automation.
Breaking Down Each Element
The complete instruction replaces the clarifying question. Because the agent can't ask, you front-load everything: the task, the success criterion, the boundary. Ambiguity that's harmless interactively is a failure mode headless.
The permission configuration replaces the approval prompt.
dontAskThe turn cap replaces the human noticing a loop. Interactively you'd see the agent retrying the same failing thing and stop it.
--max-turnsStructured output and exit codes replace the human reading the screen. A script parses cost, turn count, error status, and exit code to decide what happens next, because no one is watching.
⚡ Pro tip: When your headless needs outgrow one-shot commands — retries, streaming, complex control flow, tool orchestration in your own process — reach for the Agent SDK instead of scripting around
claude -pRetries and Logging: The Parts You Only Need Headless
Interactive sessions don't need retry logic or transcript logging, because you're right there. Headless runs need both, and this is another place the "interactive minus UI" model leaves you exposed. When a headless run hits a transient failure — a rate limit, a flaky network call, a momentary timeout — there's no human to just try again. Your wrapper has to.
[object Object],
,[object Object], attempt ,[object Object], 1 2 3; ,[object Object],
,[object Object], claude -p ,[object Object], --output-format json > out.json; ,[object Object], ,[object Object],; ,[object Object],
,[object Object], ,[object Object], >&2; ,[object Object], $((attempt * ,[object Object],))
,[object Object],What this does: Retries a failed run up to three times with backoff, then stops. It handles the transient failures a human would have simply retried, without turning a genuine, persistent failure into an infinite retry loop. Bounded retries are the headless replacement for "try that again."
Logging matters just as much. Interactively, the transcript scrolls past your eyes; headless, it vanishes unless you capture it. When an overnight run does something surprising, the saved transcript is the only way to reconstruct what happened. Capture the full output — including the agent's stated reasoning — to a timestamped log you can replay.
The append-only execution log is the mature version of this: a durable record of every action the agent took, so a failed run at 3 a.m. becomes a thing you can inspect at 9 a.m. instead of a mystery. You can't debug what you didn't record, and headless is precisely the mode where nobody saw it happen.
⚡ Pro tip: Log the agent's inputs and outputs, not just pass/fail. When a headless job produces a wrong result, the useful debugging question is "what did it see and what did it decide," and you can only answer that if you captured both. A boolean success flag tells you that it failed; the transcript tells you why, which is the part you actually need.
Variations for Different Contexts
A DevOps engineer running scheduled maintenance: bounded tasks, strict allowlists, structured output parsed for cost, exit codes gating each step. One narrow job per task.
A backend developer scripting a bulk edit across a repo: a loop that runs a bounded headless pass per file, each with a complete instruction and a turn cap, committing to a branch for review.
A platform team integrating an agent into a GitHub Action: API-key auth from secrets,
--output-format json⚡ Pro tip: Test every headless job interactively first. Run the exact task in an interactive session, watch where the agent hesitates or asks a question, and encode the answer to each of those into your headless instruction. The interactive run is your dress rehearsal — it shows you exactly which guardrails the headless version needs to replace.
Wrap the whole run in an external timeout too, as a final backstop. A turn cap bounds the agent's loop, but a hung network call or a stuck subprocess can still stall a job indefinitely. A plain
timeout 600 claude -p ...Save and Reuse This
A reliable headless cli agent isn't the interactive agent with the screen turned off — it's a bounded, fully-specified, observable program where every human guardrail has been replaced by an explicit configured one. Complete instructions replace clarifying questions, permission config replaces approval prompts, turn caps replace you noticing a loop, and structured output replaces you reading the screen.
The bounded-instruction patterns, the permission flags, the JSON-parsing and exit-code checks, the resume-dialog fix — these are identical across every headless job you'll write. Save them in PromptABCD so your next automation starts from a setup built for unattended reality instead of the "interactive minus UI" model that hangs and spirals.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
