The headless mode claude -p performs one non-interactive run and ends the process - the basis of scripts and automation. Success returns exit code 0, failure a nonzero one. A subtlety to know: startup-flag errors go to stderr, while a failure inside the run may be represented by a result in stdout. So automation checks both the return code and the structured payload - relying on one alone means one day taking a failure for a success.
The key mode for scripts is bare. The --bare flag skips auto-discovery of hooks, skills, plugins, MCP, auto memory and CLAUDE.md; it is the currently recommended mode for scripts and SDK calls and the future default for -p. It does not read subscription OAuth and the keychain: for the Anthropic API you need ANTHROPIC_API_KEY or apiKeyHelper in an explicit --settings, and cloud providers use their own credentials. Context in bare is added only explicitly - via --append-system-prompt, --settings, --mcp-config, --agents, --plugin-dir.
Alongside stands the diagnostic --safe-mode. It keeps authentication, the model, built-in tools and permissions but disables customizations. It is a middle ground for diagnostics: if a safe run works, the problem is sought in a hook, plugin, MCP, skill, memory or config customization. The difference from bare matters: bare is for a clean reproducible run in automation, safe-mode is to understand whether customization specifically breaks the work, without losing authentication and tools.
Input has limits to account for. The prompt is passed by argument or via stdin, but piped stdin is limited to 10 MB; a large log is not embedded directly but saved to a file with the path given. This is the same narrow-context rule: do not paste an unbounded CI log into the prompt but first filter or tail it, and keep the full artifact separately. Otherwise the run is more expensive, slower and risks hitting the limit out of nowhere.
Output formats determine how automation reads the result. text is the default; json is one structured envelope with result, session_id and usage and cost metadata; stream-json is newline-delimited events. It helps to see these forms side by side once. With --json-schema the business result is in structured_output, and required and additionalProperties are validated on the caller side: the format field in the schema is an annotation, not enforcement, so you do the check yourself.
Streaming output requires a careful consumer. A stream-json consumer parses event types, waits for the final result and tolerantly ignores unknown capabilities - otherwise a new version with a new event type will break the parser. Subagent events are linked via parent_tool_use_id. This is the case where the determinism of headless mode works for you only if the parser is written with room for future fields rather than rigidly for the current set.
Budget and permissions set the run's guardrails. --allowedTools uses the permission-rule syntax, --max-turns and --max-budget-usd limit the number of turns and the cost, and the process supervisor's timeout and the sandbox complete the picture. It helps to see such a closed run once: the dontAsk mode, a narrow tool allowlist, turn and budget limits, json on output. But even with all the guardrails the caller must handle a partial result and a failure - limits bound the spend, not guarantee success.
Session continuity and termination also obey rules. --continue continues the last conversation, --resume by id a specific one; the session_id is caught from JSON, and resolution is tied to the project directory. For independent CI jobs a new deterministic run is better if the history is not part of the contract. A background Bash after the final result gets about five seconds of grace, then ends; SIGTERM kills the process tree, triggers SessionEnd hooks and gives code 143. Officially --bare is preferred for scripts, and if you deliberately pull project customizations, do not call the run bare and check every source.
# bare: no auto-discovery of hooks/skills/plugins/MCP/memory/CLAUDE.md
claude --bare -p "Summarize README.md" --allowedTools Read --output-format json
# schema-constrained structured_output (validate required on the caller side)
claude --bare -p "Extract exported function names from src/index.ts" \
--allowedTools Read --output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array",
"items":{"type":"string"}}},"required":["functions"],"additionalProperties":false}'# A closed run with guardrails
claude --bare -p "Run focused tests and report failures" \
--permission-mode dontAsk \
--allowedTools "Read,Grep,Glob,Bash(npm test:*)" \
--max-turns 8 --max-budget-usd 2.00 --output-format json
# limits bound the spend, but the caller handles a partial result/failure