Claude Code Headless Mode: claude -p for Scripts and CI
Run Claude Code headless with claude -p: output formats, JSON schema, streaming, tool allowlists, bare mode, resuming sessions and copy-paste CI scripts.
By Claudekit Team Published 11 min read
Claude Code isn’t only a chat in your terminal. Add -p and it becomes a command-line tool you can pipe into, call from package.json, or run in CI. This is Claude Code headless mode, and it’s the foundation for every automation you’ll build around Claude Code.
This guide covers the flags that matter, how permissions behave without a human, structured output, and working script examples. All flags are checked against the official headless and CLI reference docs as of October 2026.
The basics
Add -p (or --print) to any claude command:
claude -p "What does the auth module do?"
Claude Code runs the full agent loop (reading files, calling tools), prints the response and exits. Exit code is 0 on success and non-zero on failure, so your scripts can branch on it. If the failure happens inside the run, such as missing authentication, the error is printed as the result on stdout.
Headless runs read stdin, so you can pipe data in:
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 inputs, write a file and reference its path in the prompt.
Permissions without a human
This is where most first headless scripts break. With Claude Code v2.1.302 or later, a claude -p run with no permission mode set starts in Manual mode. Anything that would normally prompt is denied, because there’s nobody to answer.
You have two ways to get work done:
1. Pre-approve specific tools with --allowedTools, using permission rule syntax:
claude -p "Run the test suite and fix any failures" \
--allowedTools "Bash,Read,Edit"
Scope Bash rules tightly. This commit helper allows only the git subcommands it needs (pair it with our commit message prompt for Conventional Commits):
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 *)"
The space before * matters: Bash(git diff *) matches git diff --staged, but Bash(git diff*) would also match git diff-index.
2. Set a baseline mode with --permission-mode:
| Mode | Behaviour in a -p run |
|---|---|
auto | A classifier reviews actions instead of you; requires a supported model |
acceptEdits | File writes and common filesystem commands run; other shell commands still need an allowlist entry |
dontAsk | Only pre-approved tools and read-only actions run; everything else is denied |
claude -p "Apply the lint fixes" --permission-mode acceptEdits
For the strictest CI runs, combine dontAsk with an explicit allowlist:
claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read"
When nobody can answer prompts at all, for example in a scheduled job with an SDK permission callback, add --permission-prompts none (v2.1.259+). Claude is told nobody can approve the request and not to retry it.
Use --disallowedTools for explicit deny rules. A bare tool name such as "Edit" removes the tool from Claude’s context entirely.
Output formats
--output-format controls what you get back:
text(default): plain textjson: one JSON object withresult,session_id, usage and cost metadatastream-json: newline-delimited JSON events in real time
claude -p "Summarize this project" --output-format json | jq -r '.result'
The JSON payload includes total_cost_usd and a per-model breakdown. These are client-side estimates and can differ from your bill, but they’re handy for logging spend per job.
Enforce a schema
Need machine-readable output? Add --json-schema. The validated data lands in structured_output:
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"]}' \
| jq '.structured_output'
If Claude declines or the run ends without valid output, there’s no structured_output field, so check that it exists before using it.
Stream tokens
claude -p "Explain recursion" --output-format stream-json --verbose --include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
The last line of the stream is a result message with the final text, cost and session metadata. The first event is usually system/init, which lists the model, tools, MCP servers and plugins; its mcp_server_errors and plugin_errors fields are a good CI gate for “a server silently failed to load.”
Bare mode: reproducible runs
By default, claude -p loads everything an interactive session would: hooks, skills, subagents, plugins, MCP servers, auto memory and CLAUDE.md from the working directory and ~/.claude. That’s convenient on your laptop and a reproducibility problem on CI.
--bare skips all of that auto-discovery and starts faster:
claude --bare -p "Summarize README.md" --allowedTools "Read"
Things to know:
- Auth: bare mode never reads OAuth credentials or the keychain. Set
ANTHROPIC_API_KEY(a key from the Claude Console) or anapiKeyHelperin--settings. Bedrock, Agent Platform and Foundry credentials work as usual. - Load context explicitly with flags:
--append-system-promptor--append-system-prompt-file,--settings,--mcp-config,--agents, and--plugin-dir. - Tools: Claude gets Bash, file read and file edit.
The docs recommend --bare for scripted and SDK calls and say it will become the default for -p in a future release.
Also note the security angle: without --bare, a -p session runs the hooks in a project’s .claude/settings.json and connects its .mcp.json servers, with no trust dialog, even in a folder you’ve never trusted. Don’t point non-bare headless runs at untrusted repositories.
Customize behaviour
| Flag | What it does |
|---|---|
--append-system-prompt "..." | Adds instructions while keeping Claude Code’s default prompt |
--system-prompt "..." | Replaces the default prompt entirely |
--model sonnet | Picks a model by alias (sonnet, opus, haiku, fable) or full name |
--max-turns 3 | Caps agentic turns; exits with an error at the limit |
--max-budget-usd | Stops once estimated spend reaches the amount |
--mcp-config file.json | Loads MCP servers from a file or JSON string |
--fallback-model | Falls back to other models if the primary is overloaded |
A security review script that takes a PR number:
#!/bin/bash
# review.sh: usage bash review.sh 123
gh pr diff "$1" | claude -p \
--append-system-prompt "You are a security engineer. Review for vulnerabilities." \
--output-format json
User-invoked skills work in headless mode too: include /skill-name in the prompt and Claude Code expands it. Terminal-only commands like /login don’t. See our Claude Code slash commands guide for which commands exist.
Multi-step conversations
Use --continue for the most recent conversation, or capture the session ID and --resume it:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
Resumed runs keep the full context, so the second prompt can say “that review” without repeating anything. When you continue a conversation, the JSON cost figure reports the conversation’s whole total, earlier runs included.
Practical recipes
A typo linter in package.json
{
"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 means Claude doesn’t need Bash permission to read it.
Fail a pipeline on findings
#!/bin/bash
set -euo pipefail
result=$(git diff origin/main...HEAD | claude --bare -p \
"List any hard-coded secrets in this diff. Answer only NONE or the list." \
--output-format json | jq -r '.result')
if [ "$result" != "NONE" ]; then
echo "$result"
exit 1
fi
Treat model output as advisory: pair a check like this with a deterministic secret scanner rather than replacing one.
Stopping a run
If you send SIGTERM (for example from a process supervisor), Claude Code exits with code 143, kills any running Bash command’s process tree and runs SessionEnd hooks. To end the current turn cleanly instead, send SIGINT first.
Headless mode vs the Agent SDK vs GitHub Actions
The official docs describe claude -p as “the Agent SDK via the CLI.” (For how the Agent SDK, subagents and Managed Agents differ, see Claude agents explained.) Choose based on where your code lives:
| You want to | Use |
|---|---|
| Call Claude Code from shell scripts, Makefiles, cron, any CI | claude -p |
| Embed the agent loop in a Python or TypeScript app with callbacks and typed messages | Agent SDK packages |
React to @claude mentions and PR events on GitHub | Claude Code GitHub Actions (built on the SDK) |
| Drive the agent from another language | claude -p --output-format json as a subprocess |
Headless runs consume usage like interactive ones; see our Claude Code pricing guide for how plans and API billing work, and ccusage to track tokens from local logs.
Headless mode in your Claude kit
Once your skills, subagents and hooks work interactively, claude -p is how you put them on a schedule. A mature Claude kit usually includes a few headless scripts: a pre-commit reviewer, a nightly dependency report, a changelog drafter, a first-pass log analysis when an alert fires, or regenerated API docs for changed routes. Keep them in the repo, run them with --bare plus explicit flags in CI, and they’ll behave the same on every machine.
Frequently asked questions
What is Claude Code headless mode?
It is Claude Code running non-interactively. You add -p (or --print) to the claude command, pass a prompt, and Claude Code runs the full agent loop, prints the result to stdout and exits with code 0 on success or non-zero on failure.
Why are tool calls denied in my claude -p run?
With Claude Code v2.1.302 or later, claude -p runs start in Manual mode when nothing sets a permission mode, and calls that need approval are denied because nobody can answer a prompt. Pre-approve them with --allowedTools, or start with --permission-mode auto, acceptEdits or dontAsk.
Does claude -p use my Claude subscription or an API key?
A normal claude -p run uses whatever you are logged in with. In --bare mode Claude Code never reads OAuth credentials or the keychain, so you must set ANTHROPIC_API_KEY or provide an apiKeyHelper, or use Bedrock, Agent Platform or Foundry credentials.
How is claude -p related to the Claude Agent SDK?
The official docs describe claude -p as the Agent SDK used via the CLI. The Python and TypeScript SDK packages give you the same agent loop and tools with programmatic control, such as permission callbacks and native message objects.