Original URL: https://getagenttools.com/blog/claude-p-headless/

# claude -p: the flag that turns Claude Code into a build step, and what it costs

Updated 15 Oct 2026Marek.

- claude-code
- agent-ops
- coding-agents

`claude -p` is Claude Code without the chat. You give it a prompt, it does the work with its tools, and it prints the answer to stdout. The long form is `--print`, which the CLI reference describes as "Print response without interactive mode". There is no terminal approval UI.

 

I run it from three jobs. A dispatcher starts and resumes other agent sessions. A daily reporting job sends one prompt and reads plain text back. A drafting job writes drafts, and on 5 September 2026 it got back a run that said `success` and contained nothing, which is why this post spends a section on checking output.

 

The flag belongs to [Claude Code](https://getagenttools.com/tools/claude-code/), the harness that carries most of the work on this site. That review scores the tool as a whole and lists headless runs among the reasons to pick it. This post is the flag itself, and the bill that comes with it. Flags and quotes come from Claude Code's docs, listed at the end with the dates I read them; the scripts are quoted from their source.

 

If you are changing the model behind an API integration, the [Sonnet 5.5 launch analysis](https://getagenttools.com/blog/claude-sonnet-5-5/) covers its request changes and progress messages.

  

## What is headless mode on Claude?

 

Headless mode on Claude Code is `claude -p`. The docs page for it is [Run Claude Code programmatically](https://code.claude.com/docs/en/headless), and it pitches `-p` as the command-line face of the Agent SDK, "the same tools, agent loop, and context management that power Claude Code". The first example on the page fixes a bug:

 

```bash
claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"
```

 

The `--allowedTools` part matters more than the prompt. It lets the run read, edit and use the shell without waiting for approval. One catch from the same page: start the run in auto mode and a bare `Bash` entry gets dropped, so the classifier judges every command instead. Leave the flag off and you get whatever the permission mode allows. More on that below.

 

Checking what came back is harder than it looks. The exit code is 0 on success and non-zero on failure, but a failure can land in two places. A bad flag goes to stderr before anything runs. A failure inside the run, say missing authentication, comes back as the result on stdout, and a wrapper that only watches stderr never sees it.

 

Then there is the 5 September run. My drafting job asked for a draft and got this back, from the metadata I kept:

 

```json
{
  "subtype": "success",
  "is_error": true,
  "num_turns": 1,
  "usage": {
    "output_tokens": 24000,
    "output_tokens_details": { "thinking_tokens": 24000 }
  }
}
```

 

Read the first two fields together. `subtype` is `success`. `is_error` is `true`. The process exited non-zero as well. Anything that trusts `subtype` alone would pass an empty draft to the next step. My wrapper reads the exit code first and `is_error` second. I would add a third check, one line: does `result` actually contain text? The 24000 tokens all went to thinking, which gets its own section further down.

 

## What does "headless" mean in AI and in programming?

 

Programmers used the word first. A headless browser renders pages with no window; a headless CMS has no front end of its own. With an agent, the missing piece is the approval screen. The program sends the task and reads the answer, and nothing in a terminal asks you to allow a command. You can still watch stdout scroll by. And if you attach a permission host, an Agent SDK callback or a `--permission-prompt-tool`, the run waits for that host to answer its permission requests. A plain unattended CLI run has no host, so its permissions need settling in advance.

 

## What is headless mode execution?

 

One execution is one `claude -p` process, prompt to exit. Nobody is watching it, so the brakes go on before it starts. The [CLI reference](https://code.claude.com/docs/en/cli-reference) lists two that work only in print mode: `--max-turns` caps agentic turns, and `--max-budget-usd` stops the run once API spend passes a dollar amount. The permission mode is the third. My two scripts set no turn or budget limit today. If they ever move off the laptop, a budget flag is the second change on the list in the verdict.

 

## What `--output-format json` returns

 

My reporting job reads plain text, the default, because it wants prose. The dispatcher asks for `--output-format json`, because it has to come back to a session later. It reads two fields: `result` for the text and `session_id` for the next `--resume`.

 

The same object reports `total_cost_usd` and a per-model cost breakdown, and the docs say that lets a script track spend without opening the usage dashboard. Two warnings come with it. The figures are client-side estimates and can differ from the bill. And after `--continue` or `--resume` they cover the whole conversation, earlier runs' spend included, so summing them across resumed runs counts the same spend twice. Getting the text out takes one `jq` call, straight from the docs:

 

```bash
claude -p "Summarize this project" --output-format json | jq -r '.result'
```

 

There is a third format, `stream-json`. It prints one JSON event per line: tokens as they arrive if you add `--verbose --include-partial-messages`, then a final `result` line with text, cost and session metadata. For a CI gate the docs point at the `system/init` event, which carries the session metadata and a `plugin_errors` field. It comes first in the stream unless a plugin install or a `SessionStart` or `Setup` hook runs before it.

 

The docs also show a build log piped in:

 

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

 

My reporting job passes its prompt the same way, through stdin, for a Windows reason that comes up with its code below. So the stdin rules apply to it. There is a 10 MB cap, and over it the run exits non-zero; the docs suggest writing large input to a file and putting the path in the prompt. If stdin can't be read at all, Claude Code prints a warning to stderr and runs with the prompt from the command line. On Windows, before v2.1.211, that same case crashed or quit silently.

 

## `--json-schema`: structured output you can validate

 

`result` is still free text, even inside JSON. If the next step needs fields, add `--json-schema` with a JSON Schema next to `--output-format json`. The validated object comes back on its own, in `structured_output`. The docs' example pulls function names out of a file:

 

```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"]}'
```

 

Two things to watch. A broken schema now fails the run with `Error: --json-schema is not a valid JSON Schema` and the validator's message. Before v2.1.205 it was silently ignored, so on an older install a job could run with no validation at all. And `format`, as in `"format": "email"`, is only an annotation. Claude Code doesn't enforce it, so it won't reject a bad address.

 

Only my drafting job uses the flag. The dispatcher needs nothing past `result` and `session_id`, and the reporting job wants prose. It is the contract from [What is function calling](https://getagenttools.com/blog/what-is-function-calling/), pointed at the answer instead of a tool.

 

## `claude --bare`: what it skips and what it costs

 

`--bare` is the one flag in this post that changes who pays. Per the headless page, it shortens startup by skipping auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory and CLAUDE.md. Without it, `claude -p` loads what an interactive session would, including anything configured in the working directory or `~/.claude`.

 

Everything on that list is startup content in the [context window](https://getagenttools.com/blog/claude-code-context-window/), so a bare run also starts smaller. The list is also most of the [agent harness](https://getagenttools.com/blog/what-is-an-agent-harness/) around the model; a bare run keeps the loop and the Bash, file read and file edit tools. The docs give repeatability in CI and scripts as the reason to want that: a hook in a teammate's `~/.claude` or an MCP server in a project's `.mcp.json` does not run, because bare mode never reads them. Whatever context you still need comes back through flags:

 

| To load | Flag |
| --- | --- |
| System prompt additions | `--append-system-prompt`, `--append-system-prompt-file` |
| Settings | `--settings <file-or-json>` |
| MCP servers | `--mcp-config <file-or-json>` |
| Custom agents | `--agents <json>` |
| A plugin | `--plugin-dir <path>`, `--plugin-url <url>` |

 

One thing still gets through: a directory you add with `--add-dir` brings in the skills from its `.claude/skills/` folder, though not its `.claude/commands/` or `.claude/agents/`.

 

Then the sentence that decides where a job can run: "In bare mode, Claude Code never reads OAuth credentials or the system keychain." On the Anthropic API the key comes from `ANTHROPIC_API_KEY` or an `apiKeyHelper` in the `--settings` JSON. The docs recommend `--bare` for scripted and SDK calls and say it will become the default for `-p` in a future release.

 

My jobs don't use `--bare`. Per the docs, a `-p` run without it executes the hooks in a project's `.claude/settings.json` and connects the servers in its `.mcp.json`, even in a folder you never trusted. No trust dialog, no per-server approval prompt. On my own machine I want exactly that: the [PreToolUse hook](https://getagenttools.com/blog/pretooluse-hook/) that reads every shell command in my interactive sessions reads the commands of my headless runs too, because I never pass `--bare`. In a repository I did not write, the same behavior is the reason to pass it.

 

## Permissions when nobody is at the keyboard

 

In a plain unattended run nobody answers a permission prompt in a terminal, so approvals have to be decided before the run starts. If nothing sets a permission mode, a `-p` run takes Claude Code's built-in starting mode, and per the docs that can be `auto`, so pass the mode you want rather than find out which one you got. Anything still unresolved is denied instead of asked when no permission host is attached; with a host, the run waits for its answer, as the `--permission-prompts` paragraph below explains. Three flags do the settling.

 

`--allowedTools` names tools that run without a prompt, using permission rule syntax. The docs' commit example shows the detail that bites:

 

```bash
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 trailing `  * ` turns each entry into a prefix match, so `Bash(git diff *)` allows any command that starts with `git diff`. The space before the `*` matters: per the docs, `Bash(git diff*)` would also match `git diff-index`.

 

`--permission-mode` sets a baseline instead of a list. `auto` puts a classifier in front of most actions, and `dontAsk` denies every call that would otherwise prompt. What Manual mode never asks about still runs under `dontAsk`, such as file reads in your working directories and the read-only command set, and so does anything your `--allowedTools` entries or `permissions.allow` rules cover. `acceptEdits` writes files and runs common filesystem commands such as `mkdir`, `mv` and `cp` without prompting. Apart from the read-only command set, other shell commands and network requests still need an `--allowedTools` entry or a `permissions.allow` rule.

 

`--permission-prompts` says who answers. The default, `host`, hands each prompt to an Agent SDK callback or a `--permission-prompt-tool` and waits for it. With `none` the run never consults the host: anything a permission rule or a `PermissionRequest` hook does not resolve is denied, Claude is told nobody can approve it and not to retry, and the run continues. A `-p` run with no host denies those requests either way, so there the flag's work is the no-retry instruction. It also removes tools that need a human answer, `AskUserQuestion` among them, and requires v2.1.259 or later.

 

## Sessions, limits and the way a run ends

 

`--continue` loads the most recent conversation in the current directory, and [`--resume`](https://getagenttools.com/blog/claude-resume/) with a session ID picks a specific one. The CLI reference adds a wrinkle for scheduled jobs: an interactive `--continue` skips background sessions, sessions created by `claude -p` or the Agent SDK, and sessions whose first prompt was `/loop`. `claude -p --continue` includes the `-p`, SDK and `/loop` ones. The docs' loop captures the ID from the JSON and feeds it back:

 

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

 

Since v2.1.223 the second command can run from any directory: Claude Code finds the session by ID in any project on the machine.

 

Three rules about endings, from the headless page as I read it on 15 October 2026. A background command the main conversation started, such as a dev server or a watch build, keeps the run open until the command exits or reaches its time limit. Claude then takes one more turn with the outcome. The page I read on 4 September said such a task was killed about five seconds after the final result, so check this against the version you run. Background [subagents](https://getagenttools.com/blog/claude-code-subagents/) and workflows keep the run open too, because their result is part of the output. After ten minutes of continuous idle waiting, by default, Claude Code stops what is still running (`CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`, `0` for no cap). The docs name exceptions to that cap: a running main-conversation command, and self-paced `/loop` wakeups in a run whose prompt was passed as text rather than with `--input-format stream-json`.

 

And SIGTERM exits with code 143 and leaves the turn unfinished, with no result recorded; `SessionEnd` hooks still run. A later resume leaves that turn as it is, and your next prompt drives the conversation. To have the resume continue the interrupted turn, set `CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1`. To end the turn cleanly instead, send SIGINT before you stop the process.

 

For [events into a running Claude Code session](https://getagenttools.com/blog/claude-code-channels/), check the channel server and sender controls as well as the lifetime of the receiving session.

 

## When claude -p spends every output token on thinking

 

The empty run from 5 September, shown near the top of this post, ended that way because of thinking. Its metadata puts both `output_tokens` and `thinking_tokens` at 24000: every output token went to thinking, and none was left for the draft. The model was `claude-sonnet-5`, reached through the `sonnet` alias. The JSON priced the run at $0.39. On a subscription that number is an equivalent, not a charge.

 

My notes from that day say the positive `MAX_THINKING_TOKENS` I had set was ignored. I expected a cap. On this model it is not one. The [environment variables page](https://code.claude.com/docs/en/env-vars) says it outright: "For a positive value, Claude Code ignores the number itself on adaptive reasoning models".

 

Two changes fixed my draft stage. I set `MAX_THINKING_TOKENS=0` for it. I added `--effort low` for Sonnet.

 

Check which model your alias resolves to before you copy the first change. The same page says `0` disables thinking "except on Opus 5.5, Sonnet 5.5, Haiku 5.5, and the Fable models, which can't have thinking turned off". On those models I would start with effort. You can also raise `CLAUDE_CODE_MAX_OUTPUT_TOKENS` to give the text more room. That has a price. The docs say a higher value "reduces the effective context window available before" auto-compaction triggers.

 

### Why that job runs with --safe-mode and not --bare

 

The job runs on my subscription login. That rules out `--bare`, which never reads OAuth credentials. So the call uses `--safe-mode`. The [CLI reference](https://code.claude.com/docs/en/cli-reference) describes it as a start "with all customizations disabled". CLAUDE.md, skills, plugins, hooks and MCP servers do not load. The part I needed comes next: "Authentication, model selection, built-in tools, and permissions work normally".

 

I add two more flags. `--tools ""` disables the built-in tools. `--strict-mcp-config` ignores every MCP configuration I did not pass in. The writer gets a prompt and a JSON schema. It gets no tools and none of my customizations.

 

Two things to know before you copy this. Managed settings still apply in safe mode. My own hooks do not load. Hooks set by managed policy still do. So this job is the exception to what I said above about my hook reading headless runs. With no tools there is no command for it to read.

 

## Two automations of mine that call `claude -p`

 

The first is a dispatcher for agent sessions. Its worker command is a Python list:

 

```python
[claude_exe, "-p", prompt, "--model", model, "--output-format", "json"]
```

 

`model` defaults to `haiku`. The comment says why: a fresh `-p` without `--model` takes the account's default model, about $0.40 a run by that comment's estimate. My cost rule puts 80 percent of tasks on the cheapest tier. The resume path is a second list:

 

```python
[claude_exe, "-p", "--resume", sid, prompt, "--output-format", "json"]
```

 

No `--model` there, because a resumed session inherits the model it started with, and deliberately no [`--dangerously-skip-permissions`](https://getagenttools.com/blog/dangerously-skip-permissions/); the comment calls that the safety envelope. The dispatcher caps itself at three concurrent processes, queue runs and workers counted together. It also appends its own run events to the session's event file, because a headless run writes no stop and end events of its own. Its tests never spawn the real binary: the dispatcher takes its spawn function as a dependency, and the tests pass a fake process in.

 

The second is a daily reporting job. Its whole call is:

 

```python
proc = subprocess.run(
    ["claude", "-p", "--output-format", "text"],
    input=prompt.encode("utf-8"), capture_output=True,
    timeout=CLAUDE_TIMEOUT_S, shell=(sys.platform == "win32"),
)
```

 

The prompt goes on stdin rather than into the argument list. The call runs with `shell=True` on Windows, and `cmd.exe` caps a command line at 8,191 characters per Microsoft's own note on the limit, so a long prompt has nowhere else to go. A non-zero exit raises with the first 500 characters of stderr. The job caps itself at three proposals per run, and its header comment gives headless on my subscription as the reason the model call costs nothing extra.

 

Four rules came out of those two files. Pass `--output-format json` whenever a program reads the result, because `session_id` is what lets you continue. Pass `--model` on every fresh run. Prefer `--resume` with a captured ID over `--continue`. Leave `--bare` off on a machine whose hooks you trust.

 

## The cost model: subscription or key

 

The docs say two things: plain `-p` loads the same context as an interactive session, and `--bare` "doesn't use your subscription login", so on the Anthropic API it needs `ANTHROPIC_API_KEY` or an `apiKeyHelper`. Amazon Bedrock, Google Cloud's Agent Platform and Microsoft Foundry keep reading their own provider credentials. Nothing on the headless page prices a plain `-p` run separately from an interactive one.

 

My notes add the ladder. On the laptop, plain `-p` runs as my logged-in session, so a scheduled job there is the cheapest rung. A job that moves to a server or CI needs a key, a per-call cost and a secret living on that server; `--bare` makes the run reproducible and the trade explicit. One more note about the rungs above the laptop: an MCP server that authenticates through an interactive login may not be available there, so check what each step depends on first.

 

## Does Codex have a headless mode?

 

For Codex I read two pages on 4 September 2026. One was the CLI overview. The other was the non-interactive mode page it links to. I read nothing else, and on 15 October 2026 I reread the non-interactive page for the status of `--full-auto`.

 

| Agent | Non-interactive mode | What the docs say |
| --- | --- | --- |
| Claude Code | `claude -p` | This post. `--output-format`, `--json-schema`, `--bare`, `--allowedTools`, `--max-turns`, `--max-budget-usd`. |
| Codex CLI | `codex exec` | The overview says "Use Codex interactively or call `codex exec` from repeatable workflows and pipelines" and links to a non-interactive mode page. That page documents `--json` for a JSON Lines event stream, `--output-schema` for a schema-conforming final response, and `-o` / `--output-last-message` for writing the final message to a file. On 4 September it called `--full-auto` a deprecated compatibility flag; on 15 October it says the flag is no longer supported and points scripts to `--sandbox workspace-write` or `--approve-for-me`. |

 

The surface matches. Both have a script entry point, a JSON switch and a schema switch. The defaults differ. `codex exec` starts in a read-only sandbox. It takes `--ignore-user-config` to skip `$CODEX_HOME/config.toml`. It takes `--ignore-rules` to skip the user's and the project's execpolicy `.rules` files. `claude -p` without `--bare` runs a repository's hooks and connects its MCP servers. It shows no trust dialog for either. `--bare` is the flag that opts out.

 

## The verdict

 

`claude -p` gives a script the whole agent. That means tools, loop, hooks and memory. Only the person who answered the prompts is gone. The docs supply one flag per human job: `--allowedTools` and `--permission-mode` for approvals, `--max-turns` and `--max-budget-usd` for stopping, `--json-schema` for reading the answer, `--resume` for memory between runs. `--bare` strips the rest of your setup for a repeatable result and moves the run from your login to a key.

 

If either of these two scripts moves to a server, the first change is `--bare` and a key, the second is a budget flag, because a machine that runs without me should stop without me.

 

Sources, read on 4 September 2026: [Run Claude Code programmatically](https://code.claude.com/docs/en/headless) and the [CLI reference](https://code.claude.com/docs/en/cli-reference) in Claude Code's docs, source of every flag, version number and quoted or paraphrased sentence above, except in the section on thinking and the passages on the auto-mode exception for `--allowedTools`, the starting permission mode, `dontAsk`, cost on resumed sessions, background work at exit and SIGTERM, which follow the headless page as read on 15 October 2026. The section on thinking quotes the CLI reference and the [environment variables page](https://code.claude.com/docs/en/env-vars) as read on 11 October 2026; the [Codex CLI overview](https://learn.chatgpt.com/docs/codex/cli) and its [non-interactive mode](https://learn.chatgpt.com/docs/non-interactive-mode) page, the latter reread on 15 October 2026 for `--full-auto`; Microsoft's [command prompt line string limitation](https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation) for the 8,191-character cap. The two automations are quoted from their source files on my machine.

Put this into practice

[See the tool list](https://getagenttools.com/tools/)[Train your team](https://getagenttools.com/#training)
