# Claude Code headless mode: run claude -p in scripts and pipelines

> Claude Code runs without an interactive session when you pass `-p`. It takes a prompt, runs the agent loop, prints the result and exits. The exit status is one your script can test. Nobody can answer a prompt there, so `--allowedTools` pre-approves tools. `--output-format` picks text, json or stream-json, and `--bare` makes the run behave the same on every machine.

Mindket editors · Published 2026-11-04 · Updated 2026-11-05 · https://mindket.com/learn/claude-code-advanced/claude-code-headless

## In short

- `claude -p "prompt"` prints the answer and exits 0 on success or non-zero on failure, so a shell script can branch on it.
- Nobody can answer a permission prompt in a script. Pre-approve tools with `--allowedTools` or set a mode such as `dontAsk`, or the call is refused.
- `--output-format json` returns the answer in `result` with a `session_id` and `total_cost_usd`. Add `--json-schema` and the checked data arrives in `structured_output`.
- `--bare` skips hooks, plugins, MCP servers and CLAUDE.md, so CI behaves the same everywhere. Anthropic says it will become the default for `-p`.

## Before you start

You need Claude Code, a terminal and `jq` for the JSON examples. If permission rules are new to you, read [the settings lesson](https://mindket.com/learn/claude-code-advanced/claude-code-settings) first, because a headless run has no one to answer a prompt and the rules do all the deciding.

Anthropic's page for this is titled [Run Claude Code programmatically](https://code.claude.com/docs/en/headless). It calls `claude -p` the CLI form of the Agent SDK, which lesson 4 builds on. The flags below come from that page and from [the CLI reference](https://code.claude.com/docs/en/cli-reference), read on 4 November 2026. The headless page was read again on 5 November.

## Run a prompt, pipe data in, test the exit code

Non-interactive mode reads stdin, so Claude Code works like any command-line filter. This is Anthropic's example: a build log goes in and an explanation comes out as a file.

````bash
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
````

1. Run `claude -p "What does the auth module do?"`. Claude Code prints the response and exits. `-p` is short for `--print`.
2. Pipe the build log in with the command above. Piped stdin is capped at 10MB, so write a bigger input to a file and name the path in the prompt.
3. Run `echo $?` right after. The exit code is 0 on success and non-zero when the run fails. An invalid flag is reported on stderr before the run starts.
4. Force a failure inside the run, for example by running without authentication. Claude Code prints the failure as the result on stdout, so a script should check the exit status before it trusts stdout.
5. Let Claude change files by naming tools: `claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"`.

## Nobody is there to approve: tools, modes and prompts

In Claude Code v2.1.302 or later, a `-p` run with no permission mode set starts in Manual mode. A call that needs approval is denied unless an allow rule, a hook, a `canUseTool` callback or `--permission-prompt-tool` approves it. Decide in advance what may run.

For CI, Anthropic's [permission modes page](https://code.claude.com/docs/en/permission-modes) lists an exact allowlist with `dontAsk`. It denies everything that would prompt, and only the listed command and file reads run.

````bash
claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read"
````

| Flag | What happens | Fits |
| --- | --- | --- |
| `--allowedTools "Read,Edit"` | Listed tools run without prompting. It uses permission rule syntax, so `Bash(git diff *)` allows any command that starts with `git diff` | A job whose commands you know |
| `--permission-mode acceptEdits` | File writes and common filesystem commands such as `mkdir`, `touch`, `mv` and `cp` run. Other shell commands still need an allow entry | Lint fixes and refactors |
| `--permission-mode dontAsk` | Anything that would prompt is denied. Reads in the working directory and allowed tools still run | Locked-down CI |
| `--permission-mode auto` | A classifier reviews most actions instead of you | Broad tasks with nobody watching |
| `--permission-prompts none` | Anything that would prompt is denied, and Claude is told not to retry it. Needs v2.1.259 or later | Scheduled jobs with a permission host |

## Output formats: text, json and stream-json

The default `text` output suits a human reading a terminal. A script wants `json`, which returns one object with the answer in `result`, the `session_id`, usage metadata, `total_cost_usd` and a per-model cost breakdown. Anthropic calls the cost a client-side estimate that can differ from your bill.

To get data in a shape you choose, add `--json-schema` with a JSON Schema. The checked data arrives in the `structured_output` field. An invalid schema stops the run with `Error: --json-schema is not a valid JSON Schema`, and the `format` keyword is read as a note and never enforced.

````bash
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'
````

| Format | What you get | Read it with |
| --- | --- | --- |
| `text` | Plain text, the default | A redirect to a file |
| `json` | One object with `result`, `session_id`, usage and cost. With `--json-schema`, a `structured_output` field too | `jq -r '.result'` |
| `stream-json` | Newline-delimited JSON events with a final `result` message. Add `--verbose` and `--include-partial-messages` for token deltas | `jq` over each line |

## Continue a conversation without guessing which one

`--continue` loads the most recent conversation in the current directory. `claude -p` runs are skipped by a plain `--continue`, and `claude -p --continue` includes them. For anything that runs more than once, capture the ID from the JSON output and resume that exact session.

`--resume` also takes a session name or the absolute path of a session's `.jsonl` transcript. Claude Code finds an ID in any project on the machine, so the two commands can run from different directories. When you continue, `total_cost_usd` reports the whole conversation's spend, earlier runs included. `--fork-session` starts a new ID from an old conversation, and `--no-session-persistence` saves nothing at all.

````bash
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
````

## Make CI repeatable with --bare

A plain `claude -p` loads what an interactive session loads: hooks, skills, plugins, MCP servers, auto memory and CLAUDE.md, from the working directory and from `~/.claude`. That is how a teammate's hook or a stray `.mcp.json` ends up in your CI job. Add `--bare` and Claude Code skips all of it, and Claude keeps the Bash, file read and file edit tools.

Bare mode never reads OAuth credentials or the system keychain. Set `ANTHROPIC_API_KEY`, or supply an `apiKeyHelper` in the `--settings` JSON. Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry still read their own credentials. Before v2.1.286 the limits held only partly, so check your version.

> Without `--bare`, a `-p` session runs the hooks in a project's `.claude/settings.json` and connects the servers in its `.mcp.json`, even in a folder you've never trusted.
>
> Anthropic, Claude Code documentation, [Claude Code docs: Run Claude Code programmatically](https://code.claude.com/docs/en/headless) (read 2026-11-04)

````bash
claude --bare -p "Summarize README.md" --allowedTools "Read"
````

| To load | Use |
| --- | --- |
| System prompt additions | `--append-system-prompt`, `--append-system-prompt-file` |
| Settings | `--settings` with a file path or inline JSON |
| MCP servers | `--mcp-config` |
| Custom agents | `--agents` |
| A plugin | `--plugin-dir`, `--plugin-url` |

## Put it in a script, and give it limits

This script from the page pipes a pull request diff into Claude and asks for a security review. Save it as `review.sh` and run `bash review.sh 123`. The shell swaps `"$1"` for `123`, and the review comes back as JSON with the text in `result`.

A script also needs ceilings. These are the ones the docs describe.

Versions matter too. Four releases since September changed what a `-p` run does, so check `claude --version` on the runner against them before you trust an old script.

````bash
gh pr diff "$1" | claude -p \
  --append-system-prompt "You are a security engineer. Review for vulnerabilities." \
  --output-format json
````

- `--max-turns` limits agentic turns in print mode, and there is no limit by default.
- `--max-budget-usd` stops the run when the estimated spend reaches the amount. Subagent spend counts, and spend can pass the cap, so leave headroom.
- SIGTERM ends the run with exit code 143 and records no result for the turn in progress. Send SIGINT first, or call the SDK's `interrupt()`, to end the turn cleanly.
- After the answer, a run stays open for background work it started. Idle waiting stops after 10 minutes by default, and `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` changes the cap.
- A `system/init` event in `stream-json` output lists loaded plugins and MCP servers. A non-empty `mcp_server_errors` array means a server never loaded, so a CI gate can fail on it.

**Headless changes in recent Claude Code releases**

1. **September 2, 2026** v2.1.259 adds `--permission-prompts none`. Anything that would prompt is denied at once, while the permission mode you set keeps deciding the rest.
2. **September 30, 2026** v2.1.286 makes `--bare` hold fully. Bare runs connect only the MCP servers named on the command line, send no system reminders and start no background tasks.
3. **October 30, 2026** v2.1.314 fixes a silent startup failure. A `claude -p` run that failed at startup could exit 0 with no output. It now prints the error and exits 1.
4. **November 4, 2026** v2.1.318 leaves `~/.claude.json` alone. Unattended runs, `claude -p` and SDK apps alike, stop rewriting the file when nothing in it changed.

Sources: [Claude Code release v2.1.259 on GitHub](https://github.com/anthropics/claude-code/releases/tag/v2.1.259) (read 2026-11-05), [Claude Code release v2.1.286 on GitHub](https://github.com/anthropics/claude-code/releases/tag/v2.1.286) (read 2026-11-05), [Claude Code changelog](https://code.claude.com/docs/en/changelog) (read 2026-11-05)

## Why Claude Code's creator wants --bare in every script

Boris Cherny created Claude Code, and he made `--bare` tip 12 of a thread [on X](https://x.com/bcherny/status/2038454357088457168) in March. Out of the box, `claude -p` and the TypeScript and Python SDKs search for local CLAUDE.md files, settings and MCP servers. The search is slow. A script usually wants to name what it loads instead, and he put the startup gain at up to ten times. He also called the old default a design oversight from when the team first built the SDK.

His post promised that a future version would flip the default to `--bare`. That has not happened yet. The headless page still calls it the recommended mode for scripts and a future default, so the flag is yours to add.

Add it to every CI call now. Your jobs keep the same behavior on the day the default flips. Until then, no teammate's hook or stray `.mcp.json` slips into the run.

> But for non-interactive usage, most of the time you want to explicitly specify what to load via --system-prompt, --mcp-config, --settings, etc.
>
> Boris Cherny, Creator of Claude Code, at Anthropic, [X](https://x.com/bcherny/status/2038454357088457168) (read 2026-11-05)

[![Boris Cherny (@bcherny) on X, 30 March 2026, tip 12 of his thread: use --bare to speed up SDK startup by up to 10x. By default claude -p and the SDKs search for local CLAUDE.md files, settings and MCPs.](https://mindket.com/blog/shots/x-bcherny-bare-flag.webp?v=bf0a01397968)](https://x.com/bcherny/status/2038454357088457168)

Boris Cherny on X, 30 March 2026

> A screenshot of X's own embed of the post, taken on 5 November 2026, with the time in UTC. X cuts the post short with Show more. Select it to open the post on X.

## Common mistakes

- Using `--bare` with no API key. Bare mode skips the subscription login, so the run fails to authenticate until `ANTHROPIC_API_KEY` is set.
- Reading `structured_output` without checking it exists. A run that ends without valid structured output, for example because Claude declined, has no such field.
- Running `claude -p` in a repository you did not write. Without `--bare` it runs that project's hooks and connects its `.mcp.json` servers with no trust dialog. Start with `--bare`, or pass `--setting-sources user`.
- Relying on `--continue` in a shared directory. It picks the most recent conversation there, so a second job changes which one that is. Capture the session ID.
- Stopping a job with SIGTERM and expecting a result. The turn in progress is left unfinished.

## Try it yourself

- Pipe `git diff main` into `claude -p` with `--append-system-prompt` set to a reviewer's brief. Run it once with `text` and once with `json`, and compare what a script could do with each.
- Write a shell snippet that runs a prompt with `--output-format json`, stops when the exit code is not 0, and prints `.result` and `.total_cost_usd` with `jq`.
- Run a prompt with `--output-format stream-json --verbose` and find the `system/init` event. List its plugins and MCP servers, then repeat with `--bare` and compare.
- Set `--max-turns 3` on a task that needs more turns, and note what your script receives.

## Next: Claude Code in GitHub Actions

Everything above gets harder on a CI runner, where nobody watches the run and secrets sit in its environment. The next lesson, [Claude Code in GitHub Actions](https://mindket.com/learn/claude-code-advanced/claude-code-github-actions), sets up the official action and the secrets, permissions and cost limits around it.

Hooks run your own commands at set points of a session, and `--bare` skips them. The [hooks lesson](https://mindket.com/learn/claude-code/claude-code-hooks) lists the events a hook can run on.

## Check yourself

1. A CI job runs claude -p in Claude Code v2.1.302 or later with no permission mode set, and Bash is not allowed. What happens to a shell command Claude wants to run?

   A. It runs after a prompt that the job log shows
   B. It is denied, because the run starts in Manual mode and nobody can approve it
   C. It runs, because print mode skips permission checks

   Answer: B. In that version a -p run with no mode set starts in Manual mode, and calls that need approval are denied unless a rule, hook or callback approves them.

2. Which output format returns one object with the answer in result and the session_id?

   A. text
   B. stream-json
   C. json

   Answer: C. The json format returns structured JSON with the result, the session ID and metadata, while stream-json sends newline-delimited events.

3. A job runs claude --bare -p "Summarize README.md" and fails to authenticate. What is the likeliest cause?

   A. Bare mode never reads OAuth credentials or the keychain, and ANTHROPIC_API_KEY is not set
   B. Bare mode needs the --verbose flag
   C. Bare mode works only with --output-format json

   Answer: A. Bare mode skips the subscription login, so it needs an API key in the environment or an apiKeyHelper in the settings JSON.

4. Two jobs run in the same directory and the second must continue the first one's conversation. What is the safest way?

   A. Use --continue in both jobs
   B. Pass --no-session-persistence
   C. Pass --fork-session on its own
   D. Read session_id from the JSON output and pass it to --resume

   Answer: D. --continue picks the most recent conversation in the directory, while --resume with a captured session ID names the exact one.

## Frequently asked questions

### When do I need the Python or TypeScript packages instead of claude -p?

When a script needs more than a shell can give it. Anthropic's page names tool approval callbacks and native message objects, alongside structured outputs, as what the packages add. The last lesson of this course builds on them.

### How do I change the model or the system prompt for one run?

Pass `--model` with an alias such as `sonnet`, `opus` or `haiku`, or a full model name. `--append-system-prompt` adds text to the default prompt, which suits most scripts, and `--system-prompt` replaces the whole default prompt.

### How do I get JSON out of claude -p?

Add `--output-format json`. The answer is in the `result` field, next to `session_id` and cost data. For a fixed shape, add `--json-schema` and read `structured_output`, which is missing when the run ends without valid structured output.

### Can I use skills and slash commands in a -p run?

User-invoked skills and custom commands work: put `/skill-name` in the prompt and Claude Code expands it. Built-in commands that only run in the terminal interface, such as `/login`, are unavailable. `/model` and a few others take their value as an argument.

### How do I track what a headless run costs?

Read `total_cost_usd` from the JSON output, which also has a per-model breakdown. Anthropic calls both figures client-side estimates that can differ from the bill. Add `--max-budget-usd` to stop a run at a ceiling.

### Can I resume a headless session from another directory?

Yes. `--resume` with a session ID searches the current project and its git worktrees, then every other project on the machine. You can also pass the absolute path of the session's `.jsonl` transcript.

## Sources

- [Claude Code docs: Run Claude Code programmatically](https://code.claude.com/docs/en/headless) (read 2026-11-05)
- [Claude Code docs: CLI reference](https://code.claude.com/docs/en/cli-reference) (read 2026-11-04)
- [Claude Code docs: Permission modes](https://code.claude.com/docs/en/permission-modes) (read 2026-11-04)
- [Claude Code release v2.1.259 on GitHub](https://github.com/anthropics/claude-code/releases/tag/v2.1.259) (read 2026-11-05)
- [Claude Code release v2.1.286 on GitHub](https://github.com/anthropics/claude-code/releases/tag/v2.1.286) (read 2026-11-05)
- [Claude Code changelog](https://code.claude.com/docs/en/changelog) (read 2026-11-05)
- [Boris Cherny on X: use --bare to speed up SDK startup](https://x.com/bcherny/status/2038454357088457168) (read 2026-11-05)

[Browse skills](https://mindket.com/skills)
