Skip to content

Blogclaude-code

Claude Code headless: run it from a script

Claude Code headless means running claude -p with a prompt. Pick an output format, pre-approve tools with --allowedTools, and add --bare for CI.

3hship4 min read

Share

A brushed aluminium pipe feeds into a frosted glass capsule glowing soft violet inside, with a small clay block beside it.
Contents

Claude Code headless means running claude -p with your prompt instead of opening a session. The docs call it non-interactive mode, and Claude Code exits with code 0 on success and a non-zero code when the run fails, so a script can branch on the result. Pre-approve the tools it needs and nothing waits on a prompt.

Run Claude Code headless with -p

Add the -p (or —print) flag to any claude command to run it non-interactively. The docs’ own starter call looks like this:

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

The prompt goes in and the answer comes out on stdout. The --allowedTools flag matters because a script usually has nobody to approve a tool call, so the list does the approving up front. In the docs’ bare mode example, pre-approves the Read tool so the call completes without a permission prompt.

Flag What it does with -p
--continue Continues an earlier conversation
--allowedTools Auto-approves the tools you list
--output-format Chooses text, json or stream-json
--bare Skips auto-discovery for a repeatable start

Not every flag combines with -p, though. Claude Code rejects —bg and names the conflict in the error.

Pipe data in and check the exit code

Non-interactive mode reads stdin, so Claude Code behaves like any other command-line tool in a pipe.

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

Piped stdin is capped at 10MB. Past that, Claude Code exits with a clear error and a non-zero status, and the docs suggest writing the content to a file and naming the path in the prompt instead.

One catch for scripts: an error inside the run, such as missing authentication, prints the failure as the result on stdout. A bad flag is different, since that error goes to stderr before the run starts. So a failed run can leave its message in the file you redirected to, and the exit status is the signal to trust.

Pick an output format

The --output-format flag has three values.

Value You get Use it for
text (default) Plain text Reading in a terminal or a log
json Result, session ID and metadata Parsing with jq
stream-json Newline-delimited JSON Showing tokens as they arrive

For a fixed shape, pair json with --json-schema. The structured answer then arrives in the structured_output field.

Terminal window
claude -p "Extract function names from auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'

Streaming needs two more flags: —output-format stream-json with —verbose and —include-partial-messages to receive tokens as they’re generated. The json payload also carries total_cost_usd, but the docs call both cost figures client-side estimates that can differ from your bill. For what actually drains a plan, see Claude Code usage limits.

Use bare mode so every machine behaves the same

Bare mode is --bare. It skips auto-discovery of hooks, skills, custom commands, subagents, installed plugins, MCP servers, auto memory, and CLAUDE.md, so a teammate’s setup can’t change the result.

Terminal window
export ANTHROPIC_API_KEY=...
claude --bare -p "Summarize README.md" --allowedTools "Read"

Bare mode doesn’t use your subscription login, so it needs the API key above. Anything you do want must be passed in by flag:

To load Use
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 <file-or-json>
A plugin --plugin-dir <path>, --plugin-url <url>

The security side is the stronger reason to use it. 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. If you rely on hooks, the Claude Code hooks post covers setting them up. The docs add that —bare is the recommended mode for scripted and SDK calls and will become the default for -p later. Before v2.1.286 its limits held only partly, so check your version.

Know what keeps a run open or ends it

A claude -p run can stay open after Claude finishes its turn, because it waits for background work Claude started. By default it stops whatever is still running after 10 minutes of continuous idle waiting. Set CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS to change that, or to 0 to wait without a cap. Reaching --max-budget-usd stops the remaining background work instead of waiting.

Stopping a run from outside depends on the signal:

Signal Result
SIGTERM Exit code 143, no result recorded for the turn in progress
SIGINT Ends the turn first, then you stop the process

On SIGTERM, Claude Code also terminates the process tree of any Bash command still running, per the docs. The page text used here ends partway through its subagent section, so check the full page for anything past that point. Permission handling here is limited to --allowedTools.

Verdict

Claude Code headless is claude -p plus three decisions: which tools to pre-approve, which --output-format your script can parse, and whether --bare should pin the setup. For CI, start with --bare, an API key and a narrow --allowedTools list, then read the exit status.

Try it on your own plan: read the headless docs

Sources:Claude Code docs: Run Claude Code programmatically.

Responses

Sign in to write a response

Keep reading