Headless Mode & the Agent SDK
claude -p, structured output, and budgets
- Run Claude Code non-interactively with claude -p, pipe content via stdin, and explain why files are not auto-read
- Choose the right --output-format (text/json/stream-json) and produce validated structured output with --json-schema
- Bound a headless run with --max-budget-usd and --max-turns and make it safe with --allowedTools plus --permission-mode dontAsk
- Customize the system prompt safely with --append-system-prompt instead of replacing it with --system-prompt
- Build a fan-out loop over many files with scoped tools and --bare, and account for the June 15 2026 separate SDK credit pool
Headless mode is Claude Code with no terminal UI: `claude -p "query"` runs the same agent through the SDK, prints a result, and exits — perfect for scripts, pipelines, CI, and fan-out over many files. This lesson covers print mode and its quirks, the text/json/stream-json output formats and schema-validated JSON, cost and turn budgets, safe-automation flags, system-prompt customization, and the fan-out pattern.
- 1The mental model: same agent, no terminal UI
- 2Print mode: claude -p, stdin, and the file gotcha
- 3Output formats and validated JSON
- 4Cost, turn limits, and safe automation
- 5Customizing the system prompt — safely
- 6The fan-out pattern and --bare
- 7The June 15 2026 credit pool and the Agent SDK
The mental model: same agent, no terminal UI
Everything you've learned about interactive Claude Code — the agentic loop, permission modes, CLAUDE.md, tools — is still true in headless mode. The only thing that changes is the interface. Interactive mode gives you a live terminal where you steer, approve, and watch. Headless mode (claude -p) gives you a single shot: it takes a query, runs the full agent loop through the SDK, prints the result to stdout, and exits. No prompts, no UI, no waiting for you.
That one change unlocks a whole category of uses:
- Scripts — wrap Claude in a shell script and call it like any other CLI tool.
- Pipelines — pipe logs, diffs, or data into it and pipe its output onward.
- CI/CD — run it as a build step that reviews code, generates files, or triages failures.
- Fan-out — loop it across hundreds of files, each in its own isolated run.
- The Agent SDK —
claude -pis the headless entry point; the Agent SDK is the full programmatic layer for building custom agentic workflows with complete control over orchestration, tool access, and permissions.
The mental shift is this: in interactive mode you are the verification loop and the approval authority. In headless mode there is no human in the loop — so you replace yourself with flags. Budgets replace your judgment about cost, turn limits replace your patience, allow/deny rules replace your Accept clicks, and output formats replace your eyes reading the screen. The rest of this lesson is those flags.
Key insight
-p runs through the SDK, not a stripped-down mode
claude -p (alias --print) is non-interactive and exits when done, but it is the same agent powered by the same engine — it can read files, run commands, edit code, and chain many turns. Headless does not mean less capable; it means unattended. That's exactly why the safety and budget flags matter so much here.
Print mode: claude -p, stdin, and the file gotcha
The core command is simple:
claude -p "explain what the auth middleware does"This starts a non-interactive session in the current directory, runs the agent, prints the answer, and exits. --print is the long form of -p.
Piping stdin. The real power of headless mode is composing it with other Unix tools. Anything you pipe in becomes context for the query:
tail -200 app.log | claude -p "find anomalies in these log lines"Here the last 200 log lines are fed to Claude as input, and it analyzes them. You can pipe in git diffs, command output, JSON, CSV — whatever you can produce on the command line.
The file gotcha — this trips everyone up. In interactive mode you can say "look at config.ts" and Claude will go read it with a tool. In print mode, files mentioned in your prompt are NOT auto-read. A one-shot headless run won't necessarily go fetch files just because you named them. You have two reliable ways to get file contents into the run:
- Pipe the file in as stdin:
cat config.ts | claude -p "review this". - Use an
@mention to embed the file explicitly:claude -p "review @config.ts".
If you forget this, Claude answers from the prompt text alone and you get a vague or hallucinated response. When in doubt, pipe it or @-mention it.
Combining with other invocation flags. Print mode composes with the session flags you already know — for example, continue the most recent conversation non-interactively:
claude -c -p "now write the tests for that change"Note that -p/SDK sessions don't show up in the /resume picker, but they are still real sessions and remain resumable by ID.
Watch out
Naming a file is not the same as reading it
claude -p "summarize utils.py" does not guarantee Claude reads utils.py — in print mode files are not auto-read. Either cat utils.py | claude -p "summarize" or claude -p "summarize @utils.py". This is the single most common headless-mode mistake.
Output formats and validated JSON
When a script consumes Claude's output, you rarely want prose — you want something parseable. The --output-format flag controls the shape of what lands on stdout:
--output-format | What you get | Use it when |
|---|---|---|
text (default) | Plain text — just the final answer | A human reads it, or you want the raw string |
json | A single JSON object with the result plus metadata | A script parses one structured response |
stream-json | A stream of newline-delimited JSON events as the run progresses | You want to react to events live (UIs, logs, progress) |
# Parse a single structured response with jq
claude -p --output-format json "list the exported functions in @api.ts" | jq .
# Stream events as they happen
claude -p --output-format stream-json "refactor the parser"Validated structured output with --json-schema. Output-format json gives you valid JSON, but not necessarily JSON in the exact shape your program expects. When you need the output to conform to a specific contract — fixed field names, types, required keys — pass a JSON Schema and Claude's output is validated against it. This is the headless feature that makes Claude safe to wire directly into typed code.
claude -p --json-schema '{
"type": "object",
"properties": {
"severity": { "type": "string", "enum": ["low", "medium", "high"] },
"summary": { "type": "string" },
"files": { "type": "array", "items": { "type": "string" } }
},
"required": ["severity", "summary"]
}' "triage the failing test and report severity, a summary, and affected files"Now your downstream code can rely on .severity being one of three strings and .summary always being present. Both --json-schema and --output-format's structured modes are print-mode features — they're meant for the automation path, not interactive chatting.
Tip
json vs --json-schema: validity vs conformance
--output-format json guarantees the output is well-formed JSON. --json-schema goes further and guarantees it matches your schema. If your program does result.severity and would break on a missing or misspelled field, use --json-schema — it turns Claude's free-form answer into a typed, validated contract.
Cost, turn limits, and safe automation
In interactive mode, you are the brake: you watch the cost, you get bored and hit Esc, you click Accept or deny. Headless runs have none of that, so you install the brakes as flags. There are two budget controls and two safety controls.
Budget controls — cap the spend and the work:
| Flag | Effect |
|---|---|
--max-budget-usd | Stops the run when cumulative spend reaches the given USD amount (print mode) |
--max-turns | Limits the number of agentic turns; the run errors when the limit is reached |
claude -p --max-budget-usd 5.00 --max-turns 8 "refactor the date utils and add tests"Think of --max-budget-usd as a financial circuit breaker and --max-turns as a runaway-loop guard. A misunderstood task that keeps trying could otherwise burn money or spin forever; these flags make the worst case bounded.
Safety controls — decide what runs without a human:
--allowedToolslists exactly which tools (and command patterns) may run without prompting, using permission-rule syntax.--permission-mode dontAskmakes the run auto-deny anything not in an allow rule — there is no human to prompt, so an unlisted action becomes a denial rather than a hang.
Together they are the standard recipe for safe CI automation:
claude -p \
--permission-mode dontAsk \
--allowedTools "Read" "Bash(npm test)" "Bash(git diff *)" \
--max-budget-usd 2.00 --max-turns 6 \
"run the tests and summarize any failures"This says: you may read files, run the test suite, and inspect diffs — nothing else, and if you try anything else it's denied; stop at $2 or 6 turns. That is a run you can leave unattended in a pipeline. dontAsk is the headless counterpart to interactive prompting; reserve the far more dangerous bypassPermissions (= --dangerously-skip-permissions) for throwaway containers.
Watch out
An unattended run has no Esc key
Without --max-budget-usd/--max-turns, a headless agent that misreads the task can loop or spend until something else stops it. And without --allowedTools + dontAsk, it may attempt actions you'd never approve in person. Treat these four flags as mandatory for any run that isn't supervised.
Customizing the system prompt — safely
Headless runs often need a specific persona or set of rules: "you are a terse code reviewer," "output only valid SQL," "never explain, just fix." Claude Code lets you shape the system prompt — but there's a sharp safety distinction between appending and replacing.
| Flag | Behavior |
|---|---|
--append-system-prompt "..." | Appends your text to the default prompt — safe: keeps Claude's built-in tool knowledge and safety guardrails |
--append-system-prompt-file ./extra.txt | Same, but reads the appended text from a file |
--system-prompt "..." | Replaces the entire default prompt — loses all tool guidance and safety instructions |
--system-prompt-file ./p.txt | Replaces from a file (mutually exclusive with --system-prompt) |
The rule: almost always use --append-system-prompt. The default system prompt is what teaches Claude how to use its tools correctly and what keeps its safety behavior intact. When you replace it with --system-prompt, you throw all of that away — Claude may misuse tools, ignore guardrails, or behave unpredictably, because you've deleted the instructions that made it work well.
# SAFE: add your rules on top of the defaults
claude -p --append-system-prompt "You are a strict reviewer. Flag any unhandled error path." \
"review @handler.ts"
# DANGEROUS: replaces everything, including tool/safety guidance
claude -p --system-prompt "You only output JSON." "..."Use the *-file variants when the prompt is long enough to live in version control rather than inline in a script. Reach for the replacing --system-prompt only in the rare case where you genuinely want a blank-slate model with no Claude Code behavior — and you've accepted that you're giving up the safety and tool scaffolding.
Key insight
Append adds; replace deletes
--append-system-prompt layers your instructions on top of a working agent. --system-prompt removes the agent's brain and substitutes your text — including the parts that tell it how to use tools safely. The default in scripts should be append; replace is the exception that needs a deliberate reason.
The fan-out pattern and --bare
The signature headless workflow is fan-out: run claude -p once per item in a list — one file, one module, one ticket — each in its own isolated, bounded run. Because each invocation is a fresh session, the work parallelizes cleanly and one item's context never pollutes another's.
for f in $(cat files-to-migrate.txt); do
claude -p "migrate $f to the new logging API" \
--allowedTools "Edit" "Bash(git commit *)" \
--permission-mode dontAsk \
--max-turns 4
doneNotice the discipline: every iteration is scoped (only Edit and git commit are allowed), strict (dontAsk denies anything else), and bounded (--max-turns 4). Scoping the allowed tools tightly per loop is what makes mass automation safe — a single run can only do the narrow job you gave it.
--bare for fast, minimal startup. When you're spawning many quick runs, the full startup cost — loading hooks, skills, plugins, MCP servers, auto-memory, and CLAUDE.md — adds up. --bare strips all of that:
claude --bare -p "format this JSON"In bare mode Claude skips hooks/skills/plugins/MCP/auto-memory/CLAUDE.md and exposes only Bash, Read, and Edit. It sets the CLAUDE_CODE_SIMPLE environment variable so your environment can detect it. Use --bare for fast, self-contained transformations where the project's full context would just be overhead — and don't use it when the task genuinely needs your CLAUDE.md conventions, MCP tools, or skills (it strips exactly those). Note also that claude setup-token does not work in --bare mode.
Example
Scoped fan-out review with structured output
Combine the patterns — review every changed file and emit machine-readable verdicts:
for f in $(git diff --name-only main); do
cat "$f" | claude --bare -p --output-format json \
--max-budget-usd 0.50 --max-turns 2 \
"review for bugs; reply with a one-line verdict" >> reviews.jsonl
doneEach file gets a fast, cheap, isolated review whose JSON you can post-process.
The June 15 2026 credit pool and the Agent SDK
Two things to internalize before you build automation on top of headless mode.
The separate credit pool (effective June 15, 2026). Starting June 15, 2026, Agent SDK and claude -p usage on a subscription draws from a separate monthly Agent SDK credit pool, distinct from your interactive usage limits. In plain terms: your scripted/headless runs are metered apart from your hands-on terminal sessions. This is exactly why the budget flags matter — --max-budget-usd and --max-turns keep a fan-out loop or a chatty CI job from draining that pool unexpectedly. Plan your automation's cost against this separate bucket, not your interactive allowance. (And remember Claude Code overall requires a paid plan — Pro, Max, Team Premium, Enterprise, or Console; it is not on the free tier.)
From -p to the Agent SDK. claude -p is the headless entry point — great for shell scripts and pipelines. When you outgrow a one-line invocation and need to build a real custom agent — programmatic orchestration, fine-grained tool access, custom permission handling, multi-step control flow — you move up to the Agent SDK. The SDK gives you full control over the agentic loop in code while reusing the same output formats (text/json/stream-json) you learned here.
A simple rule for choosing:
- One query, from a script or pipeline →
claude -pwith the flags from this lesson. - A custom agent with its own orchestration, tools, and permission logic → the Agent SDK.
Either way, the headless habits are the same: pipe or @-mention your inputs, pick an output format, bound the run with budgets, scope tools tightly, and append (never replace) the system prompt.
Note
Budget against the right bucket
After June 15, 2026, a runaway fan-out loop spends from your Agent SDK credit pool, not your interactive limits — so it won't show up as fewer terminal sessions, it'll quietly exhaust a separate budget. Always pair headless automation with --max-budget-usd and --max-turns.
Try it: Build a scoped, budgeted headless code reviewer
Turn claude -p into a safe, scriptable reviewer and feel every flag earn its place.
- Prove the file gotcha. Pick a real source file and run
claude -p "summarize FILENAME"(just the name, no @). Notice how vague it is. Now runcat FILENAME | claude -p "summarize this file"andclaude -p "summarize @FILENAME". Compare — this is why files must be piped or @-mentioned in print mode. - Go structured. Run a review with
--output-format json "..."and pipe it tojq .. Then write a small JSON schema withseverity(enum low/medium/high),summary(string, required), andfiles(array). Re-run with--json-schema '<your schema>'and confirm the output conforms. Try removing a required field from the task and see that the schema still enforces shape. - Install the brakes. Add
--max-budget-usd 1.00and--max-turns 4to a run on a larger task. Deliberately give an over-broad prompt and watch the run stop at the bound rather than spinning. - Make it safe. Add
--permission-mode dontAskand--allowedTools "Read" "Bash(git diff *)". Then ask it to do something outside that scope (e.g. delete a file) and confirm it's auto-denied with no prompt. - Fan out. Write a loop over
git diff --name-only mainthat runs your reviewer per changed file with--bare, the scoped tools from step 4, and your schema, appending each JSON result toreviews.jsonl. Inspect the file withjq. - Reflect. In one paragraph: which flag would you be most nervous to omit in a real CI pipeline, and why? Note how, after June 15 2026, an unbounded version of this loop would drain your separate Agent SDK credit pool — and how
--max-budget-usdprevents that.
Key takeaways
- 1claude -p (alias --print) runs the same agent non-interactively via the SDK and exits — but in print mode files are NOT auto-read, so pipe them in (tail -200 log | claude -p) or use an @ mention.
- 2Pick output with --output-format text/json/stream-json, and use --json-schema for output validated against a specific schema; both structured paths are print-mode features.
- 3Bound unattended runs with --max-budget-usd (a cost circuit breaker) and --max-turns (a runaway guard that errors at the limit).
- 4Make automation safe with --allowedTools (scope exactly which tools/commands run) plus --permission-mode dontAsk (auto-deny anything not allow-listed).
- 5Prefer --append-system-prompt (keeps tool knowledge and safety) over --system-prompt, which replaces the default and throws away that guidance; use the *-file variants for versioned prompts.
- 6Fan out with a loop over claude -p using tightly scoped --allowedTools, and --bare for minimal fast startup; from June 15 2026 SDK/-p usage draws from a separate Agent SDK credit pool, and the Agent SDK gives full orchestration/tool/permission control.
Quiz
Lock in what you learned
Check your understanding
0 / 4 answered
1.You run `claude -p "summarize the changes in deploy.yaml"` in a CI job and get a vague, generic answer. What's the most likely cause?
2.Your script does `result.severity` on Claude's output and crashes when that field is sometimes missing or renamed. Which flag fixes this most directly?
3.Why is `--append-system-prompt` strongly preferred over `--system-prompt` for headless runs?
4.You're fan-out looping `claude -p` over 300 files to apply a mechanical edit, and you want each run fast and tightly scoped. Which combination best fits, and what should you keep in mind about cost after June 15 2026?
Go deeper
Hand-picked sources to keep learning
Authoritative syntax for -p/--print, --output-format, --json-schema, --max-budget-usd, --max-turns, --allowedTools, --system-prompt variants, and --bare.
Safe headless automation: --allowedTools + --permission-mode dontAsk, budgets, and the append-don't-replace system-prompt rule.
Covers plan requirements and the June 15 2026 separate Agent SDK credit pool for -p/SDK usage.
What dontAsk and bypassPermissions actually do, and how allow rules are evaluated for unattended runs.
Issues, changelog, and release notes — useful for tracking SDK and headless behavior across versions.