Claude Code Headless Mode: claude -p for Scripts and Cron
claude -p exits 0 on success and non-zero on failure. Output formats, passing permissions, resuming sessions, catching errors, and running it from cron.
- claude -p runs a prompt once, prints the result and exits, with no permission dialogs; piped stdin is capped at 10MB.
- Output formats are text (default), json (result, session ID and metadata) and stream-json; total_cost_usd is a local estimate.
- Calls needing approval are denied unless allowed: list tools with --allowedTools (mind the space in Bash(git diff *)) or set --permission-mode.
claude -p runs a prompt once, prints the result, and exits. Unlike an interactive session, it shows no permission dialogs and never waits for a person. This guide covers the basic form, output formats, passing permissions, resuming a conversation, exit codes and error handling, scheduled runs from cron, and a note on pricing. In our test environment (macOS, Claude Code 2.1.304, on 2026-10-20) we checked claude --help and ran claude -p exactly once in an empty directory. On 2026-10-21 we rechecked claude --help with 2.1.305 and did not run claude -p again; the output below is from the 2026-10-20 run.
The basic form
Pass the prompt as an argument, or pipe it in on stdin:
claude -p "What does the auth module do?"
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 an error and a non-zero status. For anything larger, write it to a file and reference the path in your prompt.
Pick an output format with --output-format:
| Value | What you get |
|---|---|
text | The default. Just the response text |
json | JSON with the result, the session ID and metadata |
stream-json | Newline-delimited JSON events. The last line is the result |
Source: https://code.claude.com/docs/en/headless (checked 2026-10-21)
In an empty directory we ran “Reply with just OK” with --output-format json --no-session-persistence. It exited 0. The main fields looked like this (session ID and cost masked):
{
"type": "result",
"subtype": "success",
"is_error": false,
"result": "OK",
"num_turns": 1,
"session_id": "<masked>",
"total_cost_usd": <masked>,
"permission_denials": [],
...
}
Pull out the text with jq -r '.result'. total_cost_usd is an estimate Claude Code calculates locally, and it may not match your bill.
claude -p "Summarize this project" --output-format json | jq -r '.result'
Passing permissions
With -p there are no permission dialogs. A call that needs approval is denied unless something approves it. From v2.1.302, a run with no permission mode set starts in Manual (default). List the tools you want to allow with --allowedTools, using the same syntax as permission rules:
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 space before * in Bash(git diff *) matters. Without it, the rule also matches git diff-index. Instead of listing tools, you can set a baseline with --permission-mode. In claude --help the values are acceptEdits, auto, bypassPermissions, manual, dontAsk, and plan. For locked-down unattended runs such as CI, dontAsk fits: it denies everything that would otherwise prompt.
--dangerously-skip-permissions is the same as --permission-mode bypassPermissions and skips prompts. --help recommends it only for sandboxes with no internet access. Don’t use it outside a container or VM you can afford to break.
Sources: https://code.claude.com/docs/en/headless and https://code.claude.com/docs/en/cli-reference (checked 2026-10-21)
One more catch. Without --bare, -p loads hooks from the project’s .claude/settings.json and servers from .mcp.json, just like an interactive session, and it does so without a trust prompt even in a folder you’ve never trusted. The docs recommend --bare for scripted calls. --bare doesn’t use your subscription login, so it needs ANTHROPIC_API_KEY. For rule syntax, see Claude Code settings.json Permissions: A Cheat Sheet.
Resuming a conversation
--continue picks up the most recent conversation, and --resume <session_id> picks up a specific one:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
There are options for limits, too. --max-turns caps the number of agentic turns and exits with an error when it’s reached. --max-budget-usd stops once the estimated spend reaches the amount you set. Both work only with -p. --max-turns is in the docs but doesn’t appear in claude --help (we checked 2.1.304 and 2.1.305). --no-session-persistence doesn’t save the conversation, so it can’t be resumed later.
Sources: https://code.claude.com/docs/en/headless and https://code.claude.com/docs/en/cli-reference (checked 2026-10-21)
Exit codes and error handling
Per the docs, claude -p exits 0 on success and non-zero when the run fails. An invalid flag is reported on stderr before the run starts. A failure inside the run, such as missing authentication, is printed as the result on stdout. In json output, hitting --max-turns gives the subtype error_max_turns.
Sources: https://code.claude.com/docs/en/headless and https://code.claude.com/docs/en/agent-sdk/agent-loop (checked 2026-10-21)
In the shell, check three things: the exit code, is_error, and an empty result. This script summarizes yesterday’s commits in a dummy todo app:
#!/usr/bin/env bash
# nightly-summary.sh: summarize yesterday's commits of the todo app
set -euo pipefail
cd "$HOME/todo-app"
out=$("${CLAUDE_BIN:-claude}" -p "Summarize the commits from the last 24 hours in 5 bullet points." \
--output-format json --max-turns 5 \
--allowedTools "Bash(git log *)" --permission-mode dontAsk)
if [ "$(jq -r '.is_error' <<<"$out")" != "false" ]; then
echo "claude reported an error: $(jq -r '.subtype' <<<"$out")" >&2
exit 1
fi
result=$(jq -r '.result // empty' <<<"$out")
if [ -z "$result" ]; then
echo "empty result" >&2
exit 1
fi
printf '%s\n' "$result" > "$HOME/todo-app/reports/$(date +%F).md"
We tested the branches by pointing CLAUDE_BIN at stub scripts that return fixed JSON. The real claude wasn’t called.
| What the stub returned | Output | Exit |
|---|---|---|
is_error: false with a result | Writes the report | 0 |
is_error: false, empty result | empty result | 1 |
is_error: true, subtype: error_max_turns | claude reported an error: error_max_turns | 1 |
| An error on stderr and exit 1 | Error: unknown option | 1 |
Scheduled runs from cron
For anything that runs on a schedule, put it on a small Linux VPS rather than your laptop. Here is the crontab line. It assumes a Linux VPS. Our test environment (macOS) has no flock, so we didn’t run it (not run).
# crontab -e
PATH=/home/dev/.local/bin:/usr/local/bin:/usr/bin:/bin
0 6 * * * /usr/bin/flock -n /tmp/nightly-summary.lock /home/dev/bin/nightly-summary.sh >> /home/dev/logs/nightly-summary.log 2>&1
- Use absolute paths. cron’s
PATHand environment differ from your interactive shell flock -nskips a run if the previous one is still going- Send stdout and stderr to a log. Failures show up as the script’s exit 1
For environment variables that don’t reach cron, see Claude Code Env Vars Not Working. For why an unattended run shouldn’t hold keys and untrusted input together, see Permission Design for AI Agents.
A note on pricing
According to Anthropic’s help center, the Agent SDK and claude -p still draw from your subscription limits as of 2026-10-21. Previously announced changes to Agent SDK usage were paused on June 15, 2026. An October 7 update added monthly API credits to the Max and Team plans, and those credits also cover the Agent SDK and claude -p. Pricing can change, so check the official page before you subscribe.
Source: https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan (checked 2026-10-21)
For running it in CI, see Running Claude Code in CI: Costs and Traps. Plan prices are in What Claude Code Really Costs.
Summary
claude -pruns once and exits. Pass the prompt as an argument or on stdin (up to 10MB).- Read
result,is_error, andsession_idfrom--output-format jsonwithjq. - There are no permission dialogs, so pass permissions with
--allowedToolsor--permission-mode. Skip all checks only in an isolated environment. --resume <session_id>continues a run, and--max-turnsand--max-budget-usdset limits.- Catch failures with three checks: the exit code,
is_error, and an empty result. - In cron, use absolute paths and
flock, and keep a log.