Run Claude Code Headless: -p Mode, CI Pipelines, and Scripts That Don't Hang
Headless mode is how you put an agent inside a shell script or a CI pipeline. It is also where the sharp edges live, because everything the interactive UI does for you — permission prompts, visible progress, a human noticing the agent is stuck — is gone.
Claude Code has a headless mode: claude -p "your prompt" runs one agentic job with no interactive UI, prints the result, and exits. It is how you put an agent inside a shell script, a CI pipeline, or another program. It is also where most of the sharp edges live, because everything the interactive UI does for you — permission prompts, visible progress, a human noticing the agent is stuck — is gone. Here is how to run it so the failure modes are handled instead of invisible.
The basics: -p and output formats
-p (long form --print) is print mode: no TTY needed, response goes to stdout, exit code tells you whether the run errored. Three output formats:
claude -p "summarize the failing tests" # plain text claude -p "..." --output-format json # one JSON object at the end claude -p "..." --output-format stream-json --verbose # event stream as it runs
- text is for humans reading a terminal. Do not parse it in scripts.
- json is the one you want for automation: a single result object with the final response, an error flag, the session id, and usage/cost fields. Pipe it through
jqand branch on the error flag — not on grepping the prose. - stream-json emits every event as it happens. Use it when a run is long and you need liveness — a wrapper that watches the stream can kill a run that has gone quiet or is looping, instead of waiting on a result that never comes.
The permission problem — the part that actually bites
In an interactive session, a tool call the rules don't cover pops a prompt and you click allow. Headless, there is no one to ask. A call that would have prompted is simply not approved — and the agent either works around it, half-finishes, or reports failure. The symptom is a headless run that “works but does nothing”: it planned the edits and never got to write them. You have four levers, in order of preference:
- Grant exactly what the job needs with
--allowedTools:--allowedTools "Read" "Grep" "Bash(git diff *)". Rules use the same syntax assettings.jsonpermission rules — and the same traps, like rules that look right but never match; we covered those in permission rules that do nothing. - Or set a permission mode:
--permission-mode acceptEditsauto-accepts file edits but still gates the dangerous stuff.planmode is the read-only variant — useful for headless analysis jobs that should never write anything. - Put the standing policy in the repo:
.claude/settings.jsonallow rules apply to headless runs too. For a job you run daily, checked-in rules beat a growing command line. - --dangerously-skip-permissions is the blunt instrument. It does what it says — every tool call, no gate. We only consider it inside a throwaway container with nothing worth stealing: no credentials, no production access, no personal files. On a real workstation an agent with every permission and a scraped-in malicious instruction is your problem, not a hypothetical one.
Give every headless run a budget
An interactive agent that spirals gets interrupted by a human sigh. A headless one keeps going. --max-turns 20caps the number of agentic turns and exits with an error when the cap is hit — which turns “the nightly job burned tokens for four hours” into a visible failure you can alert on. Wrap the whole thing in a timeout as well (timeout 15m claude -p …); turns and wall-clock measure different runaways, and we have been saved by each when the other did not trigger.
Sessions and state between runs
Each -p invocation is a fresh session, but the transcript persists. Capture the session id from the JSON result and you can continue the same conversation later with claude --resume <id> -p "now do step two"; claude --continue picks up the most recent session in the directory. That makes multi-stage pipelines possible — plan in one run, review the plan with a human or a script, execute in a second run — without re-explaining the world each time. For everything else the agent should remember across runs, the answer is files, not sessions: CLAUDE.md and the persistence mechanisms apply to headless exactly as they do interactively.
In CI
- Authenticate with an API key in the environment — CI has no browser to run the OAuth login.
- Non-TTY is fine:
-pexists for exactly this. If a run hangs in CI but works locally, suspect a tool call waiting on permission (see above) or an MCP server that needs environment variables your runner does not have — the diagnosis path is the same as an MCP server not loading. - Treat cost as a first-class output. The JSON result includes usage — log it per run. A pipeline that runs on every push can quietly become your biggest API line item; context is where the money goes.
- Clean up after runs. Headless sessions that die mid-flight can leave MCP server processes behind, and on a busy runner or dev box they accumulate — orphaned processes is the write-up.
The headless pre-flight, on one screen
1. --output-format json → parse the result object, branch on the error flag 2. permissions decided → --allowedTools / --permission-mode / repo settings (skip-permissions only inside a disposable container) 3. --max-turns N → plus a wall-clock timeout around the whole command 4. required output defined → wrapper validates a file/JSON/test, not prose 5. usage logged per run → cost is an output, watch it 6. session id captured → --resume makes multi-stage pipelines cheap 7. after: no leaked processes on the runner
Flag reference: Anthropic's CLI reference. For orchestrating more than one of these at once — parallel headless workers with a coordinator — start from subagents, which solve the same problem inside a single session.