Table of Contents
🌏 中文版
The series entry post established that Claude Code is, at its core, an agentic loop. Normally you sit in a terminal and push that loop forward by hand — but plenty of situations don't need you in the seat: batch-fixing lint errors, reviewing every PR in CI, feeding a build log to get a plain-language explanation. Those scenarios use the same tools and the same loop with the interactive layer removed: add a -p flag, and Claude Code goes from conversation partner to a component in a Unix pipeline.
Official docs now frame this path as the entry point to the Agent SDK: claude -p is where you start, and when your needs outgrow it you move up to the Python or TypeScript SDK. The bulk of this post is about the CLI — most automation needs end right there — before covering when it's time to leave.
How headless differs from interactive mode
The difference is the interface, not the capability. claude -p "prompt" runs and exits: no TUI, no waiting for input; exit code 0 on success, non-zero on failure, so shell scripts can branch on $?. It can do everything interactive mode can — read files, run commands, connect MCP servers — because underneath it's the same loop.
Two behavioral differences matter:
- Permission for
-pstill defaults to Manual. Interactive sessions may now start in auto mode on Pro, Max, and Team plans, butclaude -pand Agent SDK sessions still use Manual as the built-in starting mode (the config value isdefault). A-psession never shows permission prompts, so unapproved actions are simply blocked. To let it act freely you must authorize explicitly via--allowedToolsor--permission-mode. - It won't load things you didn't ask for — if you tell it not to. That's
--bare, next section.
Basic usage and output formats
The simplest invocation:
claude -p "What does the auth module do?"
Non-interactive mode also reads stdin, so it composes like any other command-line tool:
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
Piped stdin is capped at 10MB; for larger input, write it to a file and reference the path.
--output-format offers three choices:
| Format | Contents | Good for |
|---|---|---|
text (default) | Plain text | Humans, or downstream consumers that just need prose |
json | One JSON object: result, session_id, usage/cost metadata | Scripts parsing results or tracking spend |
stream-json | One JSON event per line | Real-time token processing or step monitoring |
The json response includes total_cost_usd plus a per-model cost breakdown — note these are client-side estimates that may differ from your actual bill, but they let scripts track spend per invocation. To force structured output, add --json-schema:
claude -p "Extract the main function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'
The result lands in the structured_output field, and an invalid schema exits with an error rather than silently returning unstructured text. Pair with jq to pull fields:
claude -p "Summarize this project" --output-format json | jq -r '.result'
For real-time streaming use stream-json with both --verbose and --include-partial-messages:
claude -p "Explain recursion" --output-format stream-json --verbose --include-partial-messages
Each line is an event; the last line is a result message carrying the final response and cost. Filtering for text deltas with jq gives you a continuous token stream:
claude -p "Write a poem" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
Common flag combinations
--bare: the recommended mode for scripts and CI. A regular -p run loads hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, and CLAUDE.md exactly like an interactive session would. On your own machine that's a feature; on a CI runner it's an uncontrolled variable. --bare skips that auto-discovery, starts faster, and behaves identically on every machine:
claude --bare -p "Summarize README.md" --allowedTools "Read"
The tradeoff is supplying context yourself: system prompt additions via --append-system-prompt, settings via --settings, MCP servers via --mcp-config, subagents via --agents <json>. There is one exception for directories passed with --add-dir: bare mode still loads that directory's .claude/skills/, but not its commands or agents. Bare mode also is not "no tools"; Claude still has Bash, file read, and file edit tools, just without your local configuration auto-loaded. It skips OAuth login and the system keychain entirely, so Anthropic API usage needs ANTHROPIC_API_KEY; Bedrock, Google Cloud's Agent Platform, and Microsoft Foundry keep using their own provider credentials. The docs are explicit: --bare is the recommended mode for scripted and SDK calls and will become the default for -p in a future release.
--allowedTools: precise authorization. Let specific tools run without prompting, using rule syntax with prefix matching — Bash(git diff *) allows any command starting with git diff (the space before the asterisk is part of the syntax):
claude -p "Look at my staged changes and create an appropriate commit" \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"
Instead of listing tools one by one, set a baseline with --permission-mode: acceptEdits auto-approves file edits, dontAsk permits only allow-list rules and the read-only command set (good for locked-down CI), and auto hands most actions to a background classifier for review.
--max-turns: a circuit breaker. Caps the number of agentic turns and errors out when the limit is hit; no limit by default. When running untrusted tasks in batches, this is the fuse that keeps an agent from looping forever on your budget:
claude -p --max-turns 10 "Fix all ESLint errors in src/"
--continue and --resume: conversations across invocations. Headless isn't limited to one-shot calls:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
In print mode, claude -p --continue picks up the most recent resumable -p / SDK / /loop conversation, --resume takes a session ID, and since v2.1.223 both commands can run from different directories. This already brushes against "multi-turn state management" — more on that below.
Into scripts and CI
Put together, headless's natural home is build scripts and CI pipelines. The docs' example pipes a diff against main into Claude as a typo linter:
{
"scripts": {
"lint:claude": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
}
}
Piping the diff instead of letting Claude run git itself removes even the Bash permission requirement. Full GitHub Actions integration — including claude setup-token for long-lived tokens and annotations written back to the PR — is covered in the GitHub Actions post; scheduled runs (cron + claude -p) are covered in the scheduled-tasks guide.
A few CI-specific details: a run killed by SIGTERM exits with code 143 and records no result for the turn in progress — watch for this if your process supervisor judges success by exit code. Background Bash tasks started during the run (dev servers, watch builds) are terminated about five seconds after the result returns. Background subagents and workflows are waited on because they contribute to the final result; since v2.1.182 that wait is capped at ten minutes by default and can be adjusted with CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS. When --mcp-config is used with -p, v2.1.221 and later wait for pending MCP servers before the first turn, up to the 30-second MCP_TIMEOUT default. And the mcp_server_errors / plugin_errors fields in the system/init event let CI fail loudly when a server never actually loaded.
When to leave the CLI for the Agent SDK
The CLI does more than most people assume — don't rush the upgrade. But the official docs draw a clear line: the SDK ships only as Python and TypeScript packages, and for other languages the recommended way to drive the same agent loop is running the CLI as a subprocess. Conversely, if your host environment happens to be Python or TypeScript, four signals say it's time:
- Multi-turn session management.
--resumeplus shell variables survives two or three turns; maintaining hundreds of long-lived sessions in an application, with forking and resumption on demand, is what the SDK's Sessions API was designed for. - Real-time streaming. The CLI's
stream-jsonmeans "you parse NDJSON yourself"; the SDK yields native message and event objects. You still extracttext_delta, but you do not have to reconstruct session state from a plain-text pipe. - Type-safe structured output.
--json-schemareturns JSON you parse and validate yourself; the SDK can define schemas with Zod in TypeScript or Pydantic in Python and return validated data throughstructured_output. - In-process custom tools and hooks. Attaching your own
canUseToolcallback to tool approval, or wrapping your own functions withtool()/@toolas an in-process MCP server, are first-class in the SDK; from the CLI you can only approximate them from outside with--mcp-config,--agents <json>, or external processes.
One-sentence heuristic: if a script consumes the result, stay on the CLI; if your code hosts the agent, move to the SDK.
Going deeper
The Agent SDK could fill its own series, so this post stops here. To dig further, go straight to the official chapters:
- Agent SDK overview — capability summary and how the Agent SDK compares to the CLI, Client SDK, and Managed Agents
- Python SDK and TypeScript SDK — full API references
- Quickstart — your first bug-finding-and-fixing SDK agent
References
- Run Claude Code programmatically (Headless) — Claude Code Docs — Official home of
claude -p: basic usage, bare mode, structured output, streaming, continue conversations, SIGTERM behavior - CLI reference — Claude Code Docs — Complete list of
-p-related flags:--output-format,--json-schema,--max-turns,--include-partial-messages,--input-format, and more - Agent SDK overview — Claude Code Docs — SDK positioning, comparison with CLI / Client SDK / Managed Agents, and the official recommendation to drive the CLI via subprocess from other languages
- Agent SDK sessions — Claude Code Docs — Official guide to SDK multi-turn sessions, resume / fork behavior, and cross-host session storage
- Agent SDK structured outputs — Claude Code Docs — JSON Schema, Zod, Pydantic, and
structured_outputbehavior - Agent SDK custom tools — Claude Code Docs —
tool()/@tool, in-process MCP servers, and custom tool result shapes
Update Log
- 2026-08-26: Initial version, based on August 2026 official documentation (headless, cli-reference, agent-sdk overview).
- 2026-08-29: Checked against current official headless, permission modes, Agent SDK sessions / structured outputs / custom tools docs; clarified
--bareexceptions,-ppermission defaults, and SDK wording.
Loading...