31 tools· updated sep 2026

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

Updated Marek.

  • 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, 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 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, 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:

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:

{
  "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 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:

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:

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:

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, 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, so a bare run also starts smaller. The list is also most of the 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 loadFlag
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 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:

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 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:

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 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, 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 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 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:

[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:

[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; 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:

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.

AgentNon-interactive modeWhat the docs say
Claude Codeclaude -pThis post. --output-format, --json-schema, --bare, --allowedTools, --max-turns, --max-budget-usd.
Codex CLIcodex execThe 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 and the 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 as read on 11 October 2026; the Codex CLI overview and its non-interactive mode page, the latter reread on 15 October 2026 for --full-auto; Microsoft's command prompt line string limitation for the 8,191-character cap. The two automations are quoted from their source files on my machine.