Course · Claude Code · Automation
Run Claude Code headless with the -p flag
Everything that runs unattended stands on one move: running the agent without the chat - headless, in the docs’ own name for it. The goal on the payments service (the small TypeScript service this course uses as its running example) is small and concrete: get the review pass - run the tests, read the diff, judge whether anything regressed - to run from a script instead of your keyboard. A CI check and a nightly job are both just this one command, fired by something that is not you.
Print mode runs the agent like a command
Section titled “Print mode runs the agent like a command”Pass -p (long form --print) with a prompt and Claude Code runs non-interactively: it does the work, prints the result to stdout, and exits. No interface, no turns you take part in (a turn is one round of agent work between stops). The exit status is a real interface too - code 0 on success, non-zero when the run fails - so a calling script can branch on the outcome. The headless docs call this print mode.
claude -p "Run the payments test suite and summarize any failures" \ --allowedTools "Read,Bash(npm test)"That command runs to completion and writes its answer to stdout. Because it is an ordinary command-line program, it composes with everything a shell can do - pipes, cron lines, CI steps. The composability is the point.
Pipe the input, shrink the blast radius
Section titled “Pipe the input, shrink the blast radius”Print mode reads stdin, so you can hand the agent data the way you would feed any Unix tool. This is the cleanest way to give it exactly the input you want reviewed:
git diff main | claude -p "You are reviewing a payments service. Flag any change that looks like a regression in the payments flow. Be terse."Piping the diff has a quiet benefit that matters more once permissions get strict: you already ran git diff, so the agent needs no permission to run anything to see the changes. The less an unattended run has to reach for, the smaller its blast radius - an idea the next lesson builds into a posture.
One limit to know before a large diff bites you: piped stdin is capped at 10MB, and crossing the cap fails the run with a clear error and a non-zero exit. For larger input, write it to a file and point the prompt at the path instead of piping it. Both behaviors are documented in the headless docs.
Read the verdict back with –output-format
Section titled “Read the verdict back with –output-format”A script that cannot read the answer is decoration. The --output-format flag controls the shape of what comes back, with three values (headless docs):
text- the default. Plain prose, fine for a human reading a log.json- one structured object with the answer in aresultfield plus metadata:session_idand cost details. This is what a script parses.stream-json- newline-delimited JSON events as the run happens, for dashboards and long jobs. The docs pair it with--verbose, and the final line is aresultmessage.
For automation you almost always want json, because it lets the calling script pull out exactly the piece it needs:
git diff main | claude -p "Review this diff for payments regressions. Reply FOUND or CLEAN and one line of reasoning." \ --output-format json | jq -r '.result'That jq -r '.result' is the whole trick for making the agent a step in a pipeline rather than a thing you read. The JSON also carries the session_id: capture it and pass it to --resume to continue that same conversation in a later call, which is how a loop builds on its own earlier output instead of starting cold each night.
One discipline before you script that line: a pipeline’s exit status is the last command’s, so a failing claude hides behind a successful jq. Feed the line a stub CLI that exits 7 and the pipeline still reports 0 - an empty answer where the verdict should be. And a clean exit 0 is not a clean verdict: a run that completes and answers FOUND still exits 0, because the status reports the run, not the answer - the verdict rides in .result, so branch on the text, not the status. Scripts that branch on this line need set -o pipefail, or a check that .result came back non-empty, before treating silence as success.
The widget below assembles those same choices - task, output format, permission posture (the standing decisions about what a run may do without asking) - into a command. Try the read-only posture and read the flags it adds: the allowlist and the mode are doing more work than the prompt is.
When one call is not enough: the Agent SDK
Section titled “When one call is not enough: the Agent SDK”For a script or a CI step, claude -p is the simplest surface. For a longer-lived application, the official path is the Claude Agent SDK: TypeScript and Python packages wrapping the same agent loop, driven by a query() call that returns a stream of messages, with options such as allowedTools and permission mode. The SDK bundles the Claude Code binary and runs it in your process - your application still supplies the machine, the credentials, and the filesystem, exactly as the CLI does.
Use the SDK quickstart for the current option names, and the GitHub Actions guide for the first-party GitHub integration used later in this chapter.
What a -p run loads by default
Section titled “What a -p run loads by default”By default, claude -p loads the same context an interactive session would: CLAUDE.md, hooks and skills (project-supplied automation and instructions), MCP servers (external tool connections), everything in the working directory and ~/.claude. Locally that is convenient. In automation it is a liability - a teammate’s hook or a stray MCP server changes the result, so the same command answers differently on different machines.
The headless docs describe --bare, which skips auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, and CLAUDE.md; the built-in file and Bash tools remain, and you pass any needed settings explicitly. Two details matter for CI. Bare mode never reads OAuth credentials from the system keychain, so supply an API key. And the docs recommend --bare for scripted calls, with plans to make it the -p default - use it when a run must mean the same thing everywhere.
You can now run the review from a script and read its verdict programmatically. But every example above quietly pre-solved a problem: we allowed Bash(npm test) up front, or piped data in so no tool was needed at all. What happens when a headless run reaches for something it was not granted is the next lesson’s subject, and the answer is stranger than “it fails.”