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 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. 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, 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.
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt- Run
claude -p "What does the auth module do?". Claude Code prints the response and exits.-pis short for--print. - 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.
- 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. - 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.
- 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 lists an exact allowlist with dontAsk. It denies everything that would prompt, and only the listed command and file reads run.
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.
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.
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-psession runs the hooks in a project's.claude/settings.jsonand connects the servers in its.mcp.json, even in a folder you've never trusted.
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.
gh pr diff "$1" | claude -p \
--append-system-prompt "You are a security engineer. Review for vulnerabilities." \
--output-format json--max-turnslimits agentic turns in print mode, and there is no limit by default.--max-budget-usdstops 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_MSchanges the cap. - A
system/initevent instream-jsonoutput lists loaded plugins and MCP servers. A non-emptymcp_server_errorsarray means a server never loaded, so a CI gate can fail on it.
v2.1.259 adds
--permission-prompts noneAnything that would prompt is denied at once, while the permission mode you set keeps deciding the rest.
Source: Claude Code release v2.1.259 on GitHub (read November 5, 2026)
v2.1.286 makes
--barehold fullyBare runs connect only the MCP servers named on the command line, send no system reminders and start no background tasks.
Source: Claude Code release v2.1.286 on GitHub (read November 5, 2026)
v2.1.314 fixes a silent startup failure
A
claude -prun that failed at startup could exit 0 with no output. It now prints the error and exits 1.Source: Claude Code changelog (read November 5, 2026)
v2.1.318 leaves
~/.claude.jsonaloneUnattended runs,
claude -pand SDK apps alike, stop rewriting the file when nothing in it changed.Source: Claude Code changelog (read November 5, 2026)
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 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.
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
--barewith no API key. Bare mode skips the subscription login, so the run fails to authenticate untilANTHROPIC_API_KEYis set. - Reading
structured_outputwithout checking it exists. A run that ends without valid structured output, for example because Claude declined, has no such field. - Running
claude -pin a repository you did not write. Without--bareit runs that project's hooks and connects its.mcp.jsonservers with no trust dialog. Start with--bare, or pass--setting-sources user. - Relying on
--continuein 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 mainintoclaude -pwith--append-system-promptset to a reviewer's brief. Run it once withtextand once withjson, 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.resultand.total_cost_usdwithjq. - Run a prompt with
--output-format stream-json --verboseand find thesystem/initevent. List its plugins and MCP servers, then repeat with--bareand compare. - Set
--max-turns 3on 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, 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 lists the events a hook can run on.
Check yourself
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?
Show answer
B. It is denied, because the run starts in Manual mode and nobody can approve it 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.
Which output format returns one object with the answer in result and the session_id?
Show answer
C. json The json format returns structured JSON with the result, the session ID and metadata, while stream-json sends newline-delimited events.
A job runs claude --bare -p "Summarize README.md" and fails to authenticate. What is the likeliest cause?
Show answer
A. Bare mode never reads OAuth credentials or the keychain, and ANTHROPIC_API_KEY is not set Bare mode skips the subscription login, so it needs an API key in the environment or an apiKeyHelper in the settings JSON.
Two jobs run in the same directory and the second must continue the first one's conversation. What is the safest way?
Show answer
D. Read session_id from the JSON output and pass it to --resume --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
- Claude Code docs: CLI reference https://code.claude.com/docs/en/cli-reference
- Claude Code docs: Permission modes https://code.claude.com/docs/en/permission-modes
- Claude Code release v2.1.259 on GitHub https://github.com/anthropics/claude-code/releases/tag/v2.1.259
- Claude Code release v2.1.286 on GitHub https://github.com/anthropics/claude-code/releases/tag/v2.1.286
- Claude Code changelog https://code.claude.com/docs/en/changelog
- Boris Cherny on X: use --bare to speed up SDK startup https://x.com/bcherny/status/2038454357088457168

