Claude Code headless mode: claude -p for scripts and CI

Claude Code headless mode explained: claude -p, JSON and stream-json output, --allowedTools, --max-turns, --bare, resuming sessions, piping input, and use in cron and CI.

Solviera Teknoloji 9 min read

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:

FormatWhat you getUse it for
text (default)The final answer as plain textHumans, files, simple scripts
jsonOne JSON object at the endScripts that need the answer plus metadata
stream-jsonOne JSON event per line as it happensLive 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:

ModeBehavior in a headless run
dontAskAnything not pre-approved is refused. The predictable choice for scripts
acceptEditsFile edits are allowed without asking
planRead and plan only, no edits
autoA classifier reviews each action; a block denies that action and the run continues
bypassPermissionsNo 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 20 stops after that many agentic turns and exits with an error. No limit by default.
  • --max-budget-usd 5.00 stops when the estimated spend reaches the amount. It uses the client-side estimate and spend can pass the cap slightly, so leave headroom.
  • timeout from 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 runs SessionEnd hooks.

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 loadFlag
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-name in 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_denials in the JSON result and add the missing rule to --allowedTools.
  • Background work. A -p run 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-prompt replaces the default prompt entirely, including the git status snapshot. Prefer --append-system-prompt unless 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.

Questions

What is Claude Code headless mode?

It is Claude Code run non-interactively with the -p (or --print) flag. You pass a prompt, Claude does the work with the tools you allow, prints the result to stdout and exits. Exit code 0 means success and a non-zero code means the run failed, so scripts can branch on it.

How do I let claude -p edit files without prompting?

List the tools with --allowedTools, for example --allowedTools "Read,Edit,Bash(npm test *)". As of October 2026, -p runs start in Manual mode when nothing sets a mode, and tool calls that need approval are denied. Add --permission-mode dontAsk to make that explicit, or acceptEdits to allow all file edits.

How do I get JSON output from Claude Code?

Add --output-format json. The response is one JSON object with the answer in result plus session_id, usage and a total_cost_usd estimate. Pipe it to jq -r ".result" for the text. Add --json-schema to get validated data in structured_output, and check the field exists before using it.

Can I continue a headless Claude Code session?

Yes. claude -p "next step" --continue picks up the most recent conversation in the current directory. To target a specific one, capture session_id from a JSON run and pass it to --resume; Claude Code finds the session by ID from any directory on that machine.

Does headless mode use my Claude subscription or an API key?

A normal -p run uses whatever login the machine has. With --bare, Claude Code never reads OAuth credentials or the keychain, so it needs ANTHROPIC_API_KEY or an apiKeyHelper in the --settings JSON. For CI on a subscription, claude setup-token generates a long-lived OAuth token.

Bring your agents to one desk.

Download AgentVera for free; your installed CLIs are ready to go.

More posts