Headless mode lets you run Claude Code without the interactive terminal — piping prompts in, capturing structured output, and driving the agent from shell scripts, CI pipelines, and other programs. Headless mode is the foundation for automating code review, commit linting, and autonomous development workflows. This guide covers headless mode end to end: the -p flag, output formats, the Agent SDK, and battle-tested CI patterns.

Headless Mode: What You’ll Learn
This is how you turn Claude Code from an interactive pair-programmer into a scriptable automation tool. Every flag, every output format, and every SDK function in this guide serves that single goal. By the end you will be able to invoke headless mode from a one-line shell command, parse its structured JSON output, chain multi-turn sessions, and embed headless mode inside a GitHub Actions workflow.
If you already use interactive slash commands, headless mode is the same engine minus the screen. The hooks, skills, and MCP servers that power your interactive sessions are all available in headless mode — just controlled through flags instead of keystrokes.
What Is Headless Mode?
Headless mode is Claude Code’s non-interactive execution mode. Instead of opening a terminal UI where you type back and forth, it takes a prompt on the command line and runs to completion, printing the result to standard output. The entry point is a single flag: -p (long form --print).
Under the hood, it still loads the full agent loop — the same tools, the same context management, the same ability to read and edit files. The difference is purely the interface: no screen, no permission prompts, no waiting for you to type. This makes ideal for pipelines where a human is not sitting in front of the terminal.
A useful companion flag for headless mode is --bare. In a normal headless run, Claude auto-discovers project context: your CLAUDE.md memory files, installed skills, hooks, and MCP servers. The --bare flag skips all of that for a clean, fast, deterministic run. The official headless mode documentation notes that bare mode is recommended for scripted and SDK calls, and it will become the default for -p in a future release.
Your First Run
claude -p "Explain what the auth module does"
That is the entire API surface for a basic invocation. Claude reads the prompt, performs any tool use it deems necessary, and prints a text answer. Add --bare when you want headless mode to skip auto-discovery entirely:
claude --bare -p "Summarize this file" --allowedTools "Read"
Output Formats
The --output-format flag controls how headless mode returns its response. Three values are accepted: text (the default), json, and stream-json. Choosing the right format is the single most important decision for any script, because it determines whether you parse a string, a JSON object, or a live event stream.
Text Output
Plain text on stdout. Perfect for piping output into another command or writing to a file. No structure, no metadata — just the answer.
claude -p "Write a haiku about docker" > haiku.txt
JSON Output
A structured JSON object containing the result text, the session ID, the total cost in USD, and a per-model cost breakdown. Use this when your script needs to capture the session ID for later continuation, track spending, or feed the output into another JSON-consuming tool.
claude -p "List the functions in auth.py" --output-format json
The response includes fields like result, session_id, and total_cost_usd. You can also constrain output with --json-schema, which causes Claude to return a matching object inside a structured_output field — ideal for typed CI gates:
claude -p "Extract function names from auth.py"
--output-format json
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'
stream-json for live output
Newline-delimited JSON objects emitted in real time as headless mode works. This is the format to choose when you want to render a live progress indicator or react to tool-use events as they happen. It requires the --verbose flag, and you can add --include-partial-messages for token-by-token text deltas.
claude -p "Refactor the database layer"
--output-format stream-json --verbose
--include-partial-messages
Each line is a JSON object with a type field. The first event is always system/init, which reports the model, loaded tools, and MCP servers. A common inspection trick for headless mode is to count distinct event types from a real run:
echo "anything" | claude -p --output-format stream-json --verbose | jq .type | uniq -c
To extract only the assistant’s streaming text from headless mode, filter with jq using --unbuffered so the stream stays live:
claude -p "Write a poem" --output-format stream-json --verbose
--include-partial-messages |
jq --unbuffered -r 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
Piping Data Into Headless Mode
Because it reads standard input, you can pipe data into Claude the same way you pipe into grep or awk. This is the simplest and most powerful scripting pattern: build a prompt from existing data, pipe it in, and capture the result.
cat build-error.txt | claude -p 'Explain the root cause of this build error' > fix.md
git diff main | claude -p "You are a typo linter. Report filename:line for each typo."
The input format is controlled by --input-format, which accepts text (the default) or stream-json for bidirectional NDJSON control over stdio. Piped stdin in stdin is capped at 10 MB; for larger payloads, write the content to a file and reference the path in your prompt instead.
A subtle but common headless mode pitfall: use printf '%s' rather than echo when piping prompts that contain backslash sequences. The echo builtin on some shells interprets n as a newline, mangling prompts that are meant to contain literal backslashes.
Session Continuity
Headless mode is not limited to stateless one-shots. You can chain multiple invocations into a single conversation using --continue (resume the most recent session in the current directory) or --resume (resume a specific session by its ID).
claude -p "Review this codebase for performance issues"
claude -p "Now focus on the database queries" --continue
claude -p "Generate a summary of all issues found" --continue
To resume a specific session, capture its ID from a json run and pass it to --resume:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
Both commands must run from the same directory, because session lookup is scoped to the current project directory and its git worktrees. For custom session IDs when forking, combine --session-id with --resume or --continue and --fork-session.
Controlling Tools and Permissions
In an interactive session, Claude asks for permission before running a potentially dangerous tool. In headless mode, there is no human to answer that prompt, so you must decide up front how much autonomy to grant. Three flag families control permissions.
Tool Allowlists
The --allowedTools flag auto-approves specific tools so headless mode uses them without asking. The --disallowedTools flag removes tools entirely. Note that --allowedTools grants permission but does not restrict the available set — to hard-restrict which tools headless mode can even see, use --tools instead.
claude -p "Create a commit"
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"
The pattern syntax uses a trailing space-star for prefix matching: Bash(git diff *) approves any command starting with git diff. This is far safer for headless mode than blanket-approving all Bash commands.
Permission Modes
The --permission-mode flag sets a global policy for headless mode. Six values are available: default (reads only, asks for everything else), acceptEdits (auto-approves file edits and common filesystem commands), plan (read-only exploration), auto (everything with background safety checks), dontAsk (fully non-interactive, denies anything not pre-approved), and bypassPermissions (everything, no restrictions).
For automation, the documentation recommends acceptEdits and dontAsk. The dontAsk mode is particularly useful for locked-down CI: it denies anything not in your permissions.allow rules or the read-only command set, making it impossible for headless mode to go off-script.
claude --permission-mode dontAsk -p "Analyze the test suite coverage"
The --dangerously-skip-permissions flag is an alias for --permission-mode bypassPermissions. It is appropriate for headless mode only inside isolated containers or virtual machines. When running headless mode as root, Claude refuses this flag unless the IS_SANDBOX=1 environment variable is set — a guardrail against accidental foot-guns.
Bounding Runs
An autonomous run can loop indefinitely if a task is open-ended. Two flags bound the run so a single invocation never spirals out of control. The --max-turns flag caps the number of agentic turns, and --max-budget-usd sets a hard spending ceiling in US dollars.
claude -p "Refactor the auth module" --max-turns 10 --max-budget-usd 2.00
These two flags are complementary for headless mode. Use --max-turns to bound effort — how many steps the agent takes — and --max-budget-usd to bound cost. In production CI, apply both as belt and suspenders. The official GitHub Action sets a default --max-turns of 10 for headless mode for exactly this reason.
System Prompts in Headless Mode
Four flags let you control headless mode behavior by adjusting the system prompt. All of them work in both interactive and non-interactive modes. The --system-prompt flag replaces the default entirely. The --append-system-prompt flag adds text to the end of the default, preserving Claude Code’s built-in behavior while injecting your instructions into headless mode.
gh pr diff "$PR" | claude -p
--append-system-prompt "You are a security engineer. Review for vulnerabilities."
--output-format json
For longer prompts, use --append-system-prompt-file to load instructions from a file. There is also --append-subagent-system-prompt, which appends to every subagent’s system prompt including nested ones — but note this flag only applies in headless mode (-p mode).
Headless Mode and Hooks
One of the most powerful aspects of headless mode is that your existing hooks still fire. If you have a PreToolUse hook that validates bash commands or blocks dangerous operations, it runs in headless mode too — providing a safety net that no flag-based permission mode can fully replicate. This is why many teams combine acceptEdits permission mode with a custom security hook: the mode auto-approves routine edits while the hook blocks anything on a deny list.
A hook blocks a tool call by exiting with code 2. When a hook blocks in headless mode, Claude receives the block decision and adjusts its plan accordingly — it does not crash. This makes hooks the recommended enforcement layer for scripts that need fine-grained control over what the agent is allowed to do, beyond what tool allowlists can express.
The Claude Code Agent SDK
When the CLI is not enough — when you need custom tools, programmatic interrupts, multi-turn conversations in code, or tight integration with an application — the Agent SDK gives you the same agent loop that powers headless mode, programmable in Python and TypeScript.
The SDK was recently renamed. The old package claude-code-sdk is now claude-agent-sdk. Install it with pip install claude-agent-sdk for Python or npm install @anthropic-ai/claude-agent-sdk for TypeScript. The options class changed from ClaudeCodeOptions to ClaudeAgentOptions, though old names still alias for backwards compatibility.
The The query() Function
The simplest SDK entry point is the query() function. It takes a prompt and optional settings, and returns an async iterator of messages. This mirrors a single invocation but inside your code.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="What files are in this directory?",
options=ClaudeAgentOptions(allowed_tools=["Bash", "Glob"]),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
ClaudeSDKClient — Stateful Conversations
For conversations that span multiple turns, use the ClaudeSDKClient class. You send a message with query(), then consume the response stream with receive_response(), which yields each message until it emits a ResultMessage carrying the total cost.
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
async with ClaudeSDKClient(
options=ClaudeAgentOptions(allowed_tools=["Bash", "Read", "Edit"])
) as client:
await client.query("Review the auth module")
async for msg in client.receive_response():
print(msg)
The message stream includes AssistantMessage objects containing TextBlock and ToolUseBlock content, and UserMessage objects containing ToolResultBlock results. This lets your code inspect every tool call and its outcome — essential for logging, security auditing, and custom approval logic.
TypeScript SDK
The TypeScript SDK uses the same query() function with an async iterable. It accepts either a string prompt or an AsyncIterable of messages for streaming multi-turn input, giving you the same headless mode power in a typed language.
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Find and fix the bug in auth.ts",
options: { allowedTools: ["Read", "Edit", "Bash"] }
})) {
if ("result" in message) console.log(message.result);
}
For a deeper look at how all the extensibility mechanisms fit together, see the extensibility decision guide — it maps out when to reach for the SDK versus skills, hooks, or subagents.
Headless Mode in CI/CD Pipelines
Headless mode shines brightest in continuous integration. The official anthropics/claude-code-action GitHub Action wraps the SDK and handles checkout, authentication, and comment-based triggering. Store your ANTHROPIC_API_KEY as a repository secret, then invoke the action to bring headless mode into your pipeline.
name: Claude Code
on:
issue_comment:
types: [created]
jobs:
claude:
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
For automated (non-comment-triggered) runs, pass a prompt and CLI arguments through claude_args:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "Review this PR for security issues"
claude_args: |
--append-system-prompt "Follow our coding standards"
--max-turns 10
--model sonnet
For GitLab CI, install the binary in before_script, set ANTHROPIC_API_KEY as a masked variable, and invoke claude -p directly. For any CI system, the canonical pattern is to pipe a diff into claude --bare -p with --output-format json and parse the result.
export ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"
git diff main | claude --bare -p "Review this diff for typos"
--output-format json --allowedTools "Read"
Exit Codes
The headless mode CLI exits with code 0 on success and code 1 on any error — an API failure, an invalid flag, or an authentication problem. Your scripts should check the exit status before consuming stdout. A common defensive pattern wraps the invocation with a fallback so a flaky model never fails the build:
ANALYSIS=$(claude -p "$(cat /tmp/bench-prompt.txt)" --model sonnet 2>/dev/null
|| echo "Analysis unavailable.")
echo "$ANALYSIS" > /tmp/bench-analysis.txt
Note that exit code 2 is not a headless mode error code. It is a convention used by hook scripts: a PreToolUse hook exits with code 2 to block a tool call. If you write custom hooks for your runs, that is where code 2 enters the picture — not from the claude process itself.
A Worked Example: Headless Mode Commit-Message Lint
Let us build a realistic automation: a CI step that lints commit messages using Claude. The goal is to catch messages that violate conventional-commits format, contain typos, or are unclear — before they land on the main branch. This example combines piping, JSON output, exit-code checking, and a budget guard — the four patterns you will reuse in nearly every script.
First, capture the commit message and build a prompt. We pipe the raw message text into headless mode, ask for a structured verdict, and constrain the output with a JSON schema so the result is always machine-parseable.
#!/usr/bin/env bash
set -euo pipefail
MSG=$(git log -1 --pretty=%B)
SCHEMA='{"type":"object","properties":{"verdict":{"type":"string","enum":["pass","fail"]},"reason":{"type":"string"}},"required":["verdict","reason"]}'
RESULT=$(printf '%s' "$MSG" | claude --bare -p
'You are a commit-message linter following Conventional Commits. Evaluate the message. Respond with a verdict of pass or fail and a one-sentence reason.'
--output-format json
--json-schema "$SCHEMA"
--max-turns 1
--max-budget-usd 0.10
--allowedTools ""
2>/dev/null) || {
echo "warning: commit lint skipped (model unavailable)"
exit 0
}
VERDICT=$(echo "$RESULT" | jq -r '.structured_output.verdict // "pass"')
REASON=$(echo "$RESULT" | jq -r '.structured_output.reason // ""')
if [ "$VERDICT" = "fail" ]; then
echo "::error::Commit message failed lint: $REASON"
exit 1
fi
echo "Commit message passed lint."
Notice the layered safety in this script. The --max-turns 1 flag ensures Claude answers in a single pass without attempting tool use. The --max-budget-usd 0.10 flag caps the cost at ten cents. The --allowedTools "" flag means headless mode cannot touch the filesystem at all — it is purely a text evaluator. And the || { ... exit 0 } fallback means a model outage degrades gracefully instead of blocking every commit.
This pattern — pipe data into headless mode, constrain output with a schema, bound the run, and handle failure gracefully — generalizes to PR summaries, test-failure triage, changelog generation, and dozens of other CI tasks. Each one is a variation on the same skeleton.
Headless Mode: Common Mistakes to Avoid
Even experienced developers trip over the same issues when moving from interactive Claude Code to automation. These mistakes are easy to make and easy to fix once you know what to look for.
- Forgetting to bound the run. Without
--max-turnsor--max-budget-usd, an open-ended prompt can loop indefinitely and rack up cost. Always set at least one limit in CI. - Using bypassPermissions outside a sandbox in headless mode. The
--dangerously-skip-permissionsflag grants full autonomy. Use it only inside isolated containers. In CI headless mode, preferacceptEditsordontAskwith a scoped--allowedToolslist. - Ignoring headless mode exit codes. A non-zero exit means the run failed. Scripts that blindly consume stdout will process an empty or partial result. Always check
$?or useset -e. - Assuming stream-json works without verbose in headless mode. The
stream-jsonoutput format requires the--verboseflag. Without it, headless mode returns an error or no output. Add--include-partial-messagesfor token-level deltas.
Headless Mode: Best Practices
- Use bare mode for headless mode determinism. The
--bareflag skips auto-discovery of hooks, skills, and MCP servers, giving you a clean baseline. It is becoming the default for-pand is recommended for all scripted calls. - Scope tools narrowly in headless mode. Instead of
--allowedTools "Bash", useBash(git diff *)orBash(npm test *)patterns. The finer the scope, the smaller the attack surface if something goes wrong. - Pipe from files, not arguments, in headless mode. Large prompts belong in a file piped via
cat file | claude -p, not crammed into a shell argument. This avoids quoting issues and the headless mode 10 MB stdin cap is generous enough for most diffs. - Capture the session ID. Run the first invocation with
--output-format json, extractsession_idwithjq, and reuse it with--resume. This turns a chain of one-shots into a coherent conversation. - Degrade gracefully in headless mode. Wrap invocations in a fallback (
|| echo "unavailable") so a model outage never fails your build. Treat output as advisory in CI, not as a hard gate, unless you have tested the schema thoroughly.
Headless Mode: Frequently Asked Questions
Does headless mode use my hooks and skills?
Yes, by default. A normal invocation auto-discovers your CLAUDE.md memory, installed skills, hooks, and MCP servers just like an interactive session. If you want a clean run without them, add the --bare flag.
Can I use headless mode in GitHub Actions?
Yes. The official anthropics/claude-code-action@v1 wraps the Agent SDK and handles authentication, checkout, and triggering. Store your ANTHROPIC_API_KEY as a repository secret and pass prompts and CLI flags through the prompt and claude_args inputs to drive headless mode.
How do I parse output programmatically?
Use --output-format json for a single structured object, or --output-format stream-json --verbose for a live event stream. Pipe JSON output through jq to extract fields. For typed output, add --json-schema to constrain the response shape.
What is the difference between the CLI and the Agent SDK for headless mode?
The CLI (claude -p) is a command-line tool you invoke from scripts. The Agent SDK (claude-agent-sdk) is a Python or TypeScript library that gives you programmatic control over headless mode — custom tools, interrupts, multi-turn conversations in code, and tight application integration. Both use the same underlying agent loop.
How do I keep costs under control in CI?
Set --max-turns to bound the number of agentic steps and --max-budget-usd to cap spending per run. Route routine tasks to a faster, cheaper model with --model sonnet and reserve the most capable model for hard problems. Always apply both limits in CI.
Headless mode turns Claude Code into a scriptable automation engine: the -p flag runs headless mode non-interactively, --output-format controls how you parse results, tool and permission flags scope what headless mode can touch, and the Agent SDK gives you full programmatic control for application integration.