Claude Code Headless: Running claude -p in CI Without It Hanging (2026 Field Log)
My CI step running claude -p hung for the full timeout, waiting on a permission prompt no human would answer. The three flags that fix headless Claude Code, the jobs I kept, and the two I ripped back out.

On this page
Quick Answer (2026): Claude Code runs headless with claude -p "your prompt", which executes non-interactively, prints the result, and exits. There is no permission prompt in this mode, so you have to pre-authorize tools with --allowed-tools and a --permission-mode, or the run refuses to touch anything. Add --output-format json to get the response plus token usage and cost in one blob, and --max-turns so an agentic loop cannot run away inside CI. After three weeks of wiring this into a real pipeline, that is the whole trick: pre-authorize, bound the turns, read the JSON.
I write when something breaks. What broke this time: I dropped claude -p "summarize the diff" into a GitHub Actions step, watched it hang for the full timeout, and got nothing back. In the terminal that same command works. In CI it sat there waiting for a permission prompt that no human was ever going to answer. That failure is the entire lesson of headless mode, so this log is built around it.
What headless mode actually is
claude -p (long form --print) is Claude Code with the interactive shell removed. It takes one prompt, runs the agent loop, prints the final response to stdout, and exits with a status code. The headless docs call it print mode, and it is the same engine that powers the SDK and the official GitHub Action underneath.
The plainest version:
claude -p "List every TODO comment in src/ and group them by file"
You can pipe context in on stdin, which is how most of my real uses look:
git diff --staged | claude -p "Write a one-paragraph commit message for this diff"
That second one is genuinely useful and completely safe, because it only reads what you piped and writes text back. The trouble starts the moment the prompt implies a tool.
The permission wall nobody warns you about
Interactive Claude Code asks before it edits a file or runs a command. Headless Claude Code cannot ask, because nobody is there. So the default posture is to refuse anything that needs a permission it does not already have. My hung CI job was Claude waiting, forever, for an approval that headless mode structurally cannot request.
The fix is to grant the tools up front:
claude -p "Run the test suite and tell me what failed" \
--allowed-tools "Bash(npm test:*)" \
--permission-mode acceptEdits \
--max-turns 6 \
--output-format json
Three of those flags matter:
--allowed-toolsis an explicit allowlist.Bash(npm test:*)lets it runnpm testand nothing else shell-shaped. This is the same matcher grammar as the interactiveallowlist I wrote about in my permissions field log, and in headless mode it stops being optional.--permission-mode acceptEditstells it to stop asking about file edits. In a locked-down CI container some teams go further with a bypass mode, but I keepacceptEditsand a tight allowlist rather than opening the whole door.--max-turns 6is the seatbelt. Without it, an agentic loop that keeps deciding it needs one more step will happily burn your entire runner budget. Six is enough for the small jobs I give it and cheap to cap.
Read the JSON, not the vibes
--output-format json is the flag that turned headless from a toy into something I trust. Instead of loose text, you get a structured object with the result, the number of turns, and the token usage and dollar cost of the run:
claude -p "Summarize CHANGELOG.md in 5 bullets" --output-format json \
| jq '{result, cost: .total_cost_usd, turns: .num_turns}'
That is how I keep an eye on spend without guessing. claude code cost is a top search for a reason, and the honest answer for automation is: stop eyeballing it, log total_cost_usd from every headless run and sum it. There is also --output-format stream-json if you want to consume events as they happen, which matters for anything long enough that you want a progress signal.
The three headless jobs I actually kept
- Commit-message drafts.
git diff --staged | claude -pon a git alias. Zero tools, zero risk, saves me a sentence of thinking fifty times a day. - A PR summary step in CI. Reads the diff, writes a plain-language summary into the job output. Allowlist is read-only,
--max-turns 3, JSON output logged for cost. This is the one that hung until I pre-authorized it. - A nightly "what looks broken" pass. Runs the test suite headless, and if it fails, drafts a short triage note.
--allowed-tools "Bash(npm test:*)",--max-turns 6. It does not fix anything. Headless code that edits and pushes on its own is a footgun I am not ready to hold.
The two I ripped back out
- A headless job that auto-committed formatting fixes. It worked until the week it "fixed" a file into a broken state and pushed it while I was asleep. Now formatting is a
PostToolUsehook in my interactive sessions, not an autonomous headless push. Let the agent draft; let a human merge. - A giant single prompt doing five things. Splitting it into three small
claude -pcalls, each with its own tight allowlist and turn cap, was cheaper, faster, and far easier to debug when one step misbehaved. Small headless jobs beat one clever one.
If you wire in exactly one thing
Put --max-turns and --allowed-tools on every headless invocation, and log total_cost_usd from the JSON. The CLI reference has the full flag list, but those three are what stand between "a useful CI step" and "a runner that hangs, then bills you." Pre-authorize, bound the turns, read the JSON. That is the field log.
Written by
M. PatelBackend dev. Writes when something breaks. Currently shipping a B2B agent product with one cofounder.
Frequently asked questions
How do I run Claude Code headless?
Run claude -p "your prompt" (long form --print). It executes non-interactively, prints the final response to stdout, and exits with a status code. You can pipe context in on stdin, for example: git diff --staged | claude -p "Write a commit message".
Why does claude -p hang in CI?
Headless mode cannot show permission prompts, so by default it waits or refuses when a prompt implies a tool it has not been granted. In CI there is no human to approve, so the job hangs until timeout. Fix it by pre-authorizing tools with --allowed-tools and setting a --permission-mode such as acceptEdits.
How do I see how much a headless Claude Code run costs?
Add --output-format json. The structured result includes total_cost_usd and token usage alongside the response, so you can log and sum spend per run instead of guessing. Pipe it through jq to pull out .total_cost_usd.
How do I stop a headless Claude Code run from looping forever?
Use --max-turns to cap the number of agentic turns, for example --max-turns 6. Without it, an agent that keeps deciding it needs one more step can burn your entire CI runner budget.
Keep reading
Claude Code Permissions: The settings.json I Actually Run (2026 Field Log)
The three arrays, the precedence rule nobody explains, the two gotchas that cost me an afternoon, and the exact settings.json allow, ask, and deny block I run now.
Claude Code hooks: the 4 I kept, the 2 I deleted (2026)
I ran Claude Code hooks daily for a month. Four earned their place, two got deleted, and one quietly made my workflow worse. The honest field log.
MCP server in production: 30-day field log (June 2026)
A 30-day field log of running an MCP server in production: four failure modes the tutorials skip, honest latency and cost numbers, and when an MCP server is the wrong tool.

