Claude Code headless mode is the -p flag. claude -p "your prompt" runs Claude Code once without the interactive interface: it reads your prompt (and anything you pipe in), works with the tools you allow, prints the result and exits. Exit code 0 means success, anything else means the run failed. That makes it a normal command-line tool you can put in shell scripts, cron jobs, git hooks and CI pipelines.
This guide covers the flags you’ll actually use: output formats, tool permissions, turn and budget limits, --bare, continuing sessions and piping data, followed by ready-to-use patterns for scripts, cron and CI, and when to move to the Agent SDK. Every flag here is checked against the official CLI reference as of October 2026.
The basics
claude -p "What does the auth module do?"
-p and --print are the same flag. Most interactive options combine with it, such as --model, --effort, --append-system-prompt and --mcp-config. A few options exist only in print mode, among them --max-turns, --max-budget-usd, --json-schema and --no-session-persistence.
Two details matter for scripts:
- If you pass an invalid flag, the error goes to stderr before the run starts.
- If something fails inside the run, such as missing authentication, the failure is printed as the result on stdout. Check the exit code, not just the output.
Pipe data in, redirect results out
Headless mode reads stdin, so logs, diffs and files flow in like with any Unix tool:
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
git diff main | claude -p "Review this diff for bugs. List each as file:line and one sentence."
Piped stdin is capped at 10MB. Above that, Claude Code exits with an error, so for big inputs write the content to a file and name the path in the prompt instead.
You can also wire it into package.json, as the official docs do with 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.\""
}
}
Choose an output format
--output-format takes three values:
| Format | What you get | Use it for |
|---|---|---|
text (default) | The final answer as plain text | Humans, files, simple scripts |
json | One JSON object at the end | Scripts that need the answer plus metadata |
stream-json | One JSON event per line as it happens | Live progress, custom UIs, logging |
The JSON object carries the answer in result, plus session_id, usage figures and total_cost_usd. That cost is a client-side estimate and can differ from your bill.
claude -p "Summarize this project" --output-format json | jq -r '.result'
Structured output with —json-schema
When a script needs data rather than prose, pass a JSON Schema. The validated result 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"]}'
If the run ends without valid structured output, for example because Claude declined, the field is missing. Check for it before using it:
out=$(claude -p "Extract the main function names from auth.py" --output-format json --json-schema "$(cat schema.json)")
echo "$out" | jq -e '.structured_output' > /dev/null || { echo "no structured output" >&2; exit 1; }
Streaming events
stream-json requires --verbose. Add --include-partial-messages to get tokens as they’re generated. This prints just the streaming text:
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'
The first event is normally system/init, with the model, tools, MCP servers and plugins. The last line is a result message with the final text, cost and session metadata. Retries show up as system/api_retry events. A useful CI gate: when you pass --mcp-config, a server entry that fails validation is skipped without failing the run, and it appears in mcp_server_errors on the init event. Fail the job if that array isn’t empty.
Control what the run may do
There is nobody to answer a permission prompt in a script, so decide up front what Claude may do.
--allowedTools pre-approves tools using permission rule 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 *)"
The space before * matters: Bash(git diff *) matches commands starting with git diff, while Bash(git diff*) would also match git diff-index.
--permission-mode sets the baseline for everything not listed. As of October 2026 (Claude Code v2.1.302 and later), a -p run starts in Manual mode when nothing sets a mode, and a call that needs approval is denied. Useful values:
| Mode | Behavior in a headless run |
|---|---|
dontAsk | Anything not pre-approved is refused. The predictable choice for scripts |
acceptEdits | File edits are allowed without asking |
plan | Read and plan only, no edits |
auto | A classifier reviews each action; a block denies that action and the run continues |
bypassPermissions | No checks at all. Only inside a disposable container or VM |
--tools restricts which built-in tools exist at all, and --disallowedTools adds deny rules. --permission-prompts none tells Claude that nobody can approve requests so it doesn’t keep retrying them; denials are listed in permission_denials on the final result. Why bypassing permissions on your own machine is risky is covered in Claude Code skip permissions.
Put limits on every unattended run
--max-turns 20stops after that many agentic turns and exits with an error. No limit by default.--max-budget-usd 5.00stops when the estimated spend reaches the amount. It uses the client-side estimate and spend can pass the cap slightly, so leave headroom.timeoutfrom your shell or CI step is still worth having. If you stop a run with SIGTERM, Claude Code exits with code 143, records no result for the unfinished turn and still runsSessionEndhooks.
Use —bare for reproducible runs
Without --bare, claude -p loads the same things as an interactive session: hooks, skills, plugins, MCP servers, auto memory and CLAUDE.md, from the working directory and from ~/.claude. That means the same command behaves differently on your laptop and on a CI runner.
claude --bare -p "Summarize README.md" --allowedTools "Read"
--bare skips all of that discovery and starts faster. You pass in only what the run needs:
| To load | Flag |
|---|---|
| Extra instructions | --append-system-prompt, --append-system-prompt-file |
| Settings | --settings <file-or-json> |
| MCP servers | --mcp-config <file-or-json> |
| Custom agents | --agents <file-or-json> |
| A plugin | --plugin-dir <path> |
Bare mode never reads OAuth credentials or the system keychain. With the Anthropic API, set ANTHROPIC_API_KEY or provide an apiKeyHelper in the --settings JSON. The official docs recommend --bare for scripted calls and say it will become the default for -p in a future release, so writing it explicitly now keeps your scripts stable when that happens.
One more reason to care: without --bare, a -p run in a freshly cloned repo runs that repo’s project hooks and .mcp.json servers without a trust dialog. Don’t point a non-bare headless run at code you haven’t reviewed.
Continue and resume sessions
Headless runs are saved like interactive ones, so you can chain steps:
# First request
claude -p "Review this codebase for performance issues"
# Continue the most recent conversation
claude -p "Now focus on the database queries" --continue
claude -p "Generate a summary of all issues found" --continue
When several runs happen at once, --continue may pick the wrong one. Capture the session ID instead:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
--resume finds the session by ID from any directory on the machine, and also accepts a name or the path to a session’s .jsonl transcript. --fork-session starts a new branch from the resumed conversation instead of appending to it, and --no-session-persistence keeps one-off runs from being saved at all.
Patterns for scripts, cron and CI
A loop over files
The official best-practices page has a good template for a mechanical change across many files:
for file in $(cat files.txt); do
claude --bare -p "Migrate $file to the new logger API and commit. Reply OK or FAIL." \
--allowedTools "Read,Edit,Bash(git add *),Bash(git commit *)" \
--permission-mode dontAsk \
--max-turns 20
done
Run it on three files first, read the commits, fix the prompt, then run the rest. A run that hits the turn limit exits with an error, which your loop can log.
A review script
gh pr diff "$1" | claude -p \
--append-system-prompt "You are a security engineer. Review for vulnerabilities." \
--output-format json
Save it as review.sh and run bash review.sh 123; the review text is in result.
A nightly cron job
# crontab -e
0 2 * * * cd /srv/app && claude --bare -p "Run the test suite, fix failing tests, commit on the current branch. Do not push." --allowedTools "Read,Edit,Bash(npm test *),Bash(git add *),Bash(git commit *)" --permission-mode dontAsk --max-turns 30 --output-format json >> /var/log/claude-nightly.jsonl 2>> /var/log/claude-nightly.err
Cron runs with a minimal environment, so make sure claude is on its PATH and ANTHROPIC_API_KEY is set for that user. Run it on a branch or in a worktree, not on main. If you add --worktree, note that -p --worktree runs aren’t cleaned up for you; remove them with git worktree remove <path>. For the full overnight setup with budgets and morning review, see How to run Claude Code overnight, and for timers on each OS, How to schedule Claude Code tasks.
CI pipelines
In CI, use --bare, keep the API key in the CI secret store, and treat the exit code as the step result. For GitHub specifically, the official action wraps this for you, with PR comments and @claude mentions: see Claude Code GitHub Actions. If you’re on a subscription rather than an API key, claude setup-token prints a long-lived OAuth token for CI and scripts; it isn’t saved, so store it as a secret right away. Remember that --bare ignores OAuth, so a subscription token needs a non-bare run.
Pitfalls
- Skills and slash commands. User skills work if you put
/skill-namein the prompt. Commands that only exist in the terminal interface, such as/login, don’t. - Silent tool denials. If Claude says it couldn’t do something, check
permission_denialsin the JSON result and add the missing rule to--allowedTools. - Background work. A
-prun may stay open to wait for background commands or subagents Claude started; by default it stops waiting after 10 minutes of idle time. - Replacing the system prompt.
--system-promptreplaces the default prompt entirely, including the git status snapshot. Prefer--append-system-promptunless you know you want a blank slate.
When to move to the Agent SDK
claude -p is the Agent SDK’s engine behind a CLI. Once your script starts parsing streams, handling approvals or managing many sessions, switch to the Agent SDK for Python or TypeScript. It gives you native message objects, tool-approval callbacks and structured outputs in code, without shelling out. The Claude Code best practices post covers what to put in place before any run goes unattended.
Doing it with AgentVera
Headless runs are fire-and-forget; sometimes you want the same automation with a terminal you can watch. In AgentVera, scheduled tasks (on every plan) send a prompt to an agent on a cron schedule while the app is open, and the agent runs in a real pane you can open and steer. Automations (Pro and Team) start an agent when CI goes red or a PR gets a comment, and never push to the default branch. The CI/CD center lists GitHub Actions, GitLab and Jenkins runs and can hand a failed log to an agent. Night shift (Pro and Team) runs queued work overnight in a worktree on a night/<date>-<slug> branch, commits without pushing and leaves a morning report. Download AgentVera if you want those runs visible instead of buried in a log file.
Wrapping up
The core recipe for headless Claude Code is short: claude --bare -p "clear task with a check", an explicit --allowedTools list, --permission-mode dontAsk, --max-turns, --output-format json, and an exit code your script respects. Start small, read what the first runs produce, and only then put it on a timer.