Claude Code headless mode title card with a terminal running claude --bare -p "fix the failing test" with --output-format json piped to jq.
All articles

Claude Code headless mode: run claude -p in scripts and CI

How Claude Code headless mode works: claude -p, --bare, JSON output, --json-schema, tool approval, permission modes and resuming sessions in scripts.

Claude Code headless mode means running Claude Code without its interactive terminal interface: you pass a prompt with -p, Claude works through it, prints the result and exits. That makes it a normal command-line tool you can call from a shell script, a git hook, a package.json script or a CI job.

Anthropic's documentation now files this under running Claude Code programmatically and the Agent SDK, but the page still lives at the headless docs URL, and the flags are the same ones most people mean when they search for headless mode. This guide covers the flags you need, how to get output a script can parse, how to approve tools without a person at the keyboard, and how to continue a conversation across several calls. Everything below was checked against the official documentation as of October 2026.

What headless mode is

In an interactive session you type, Claude asks for permission, you approve, and the conversation stays open. In headless mode the whole exchange is one command:

claude -p "What does the auth module do?"

-p is short for --print. Claude Code loads the same tools and agent loop it uses interactively, answers, and returns control to your shell. A few rules make it script friendly:

  • It exits with code 0 on success and a non-zero code when the run fails, so if statements and CI steps can branch on it.
  • 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.
  • It reads stdin, so you can pipe data in and redirect the answer out. Piped stdin is capped at 10MB; for larger input, write a file and name its path in the prompt.
Diagram of claude -p as one step in a shell pipeline: a build log enters on stdin, claude -p runs its tools and agent loop without prompts, then writes the answer to stdout and exits with code 0 on success.
Headless mode turns Claude Code into an ordinary command: stdin in, answer on stdout, and an exit code your script can branch on.

Your first claude -p run

Start with something read-only so you can see the shape of the output:

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

The prompt comes from the argument, the build log from stdin, and the explanation lands in output.txt. Nothing in the repository changes, because no tool that edits files was approved.

Not every interactive feature works under -p. User-invoked skills and custom commands do: put /skill-name in the prompt and Claude Code expands it before running. Built-in commands that only make sense in the terminal interface, such as /login, are not available. A few commands accept their value as an argument instead, for example /model sonnet.

Start faster and reproducibly with --bare

By default a -p run loads the same context an interactive session would: your CLAUDE.md, hooks, skills, subagents, plugins, MCP servers and auto memory from the working directory and ~/.claude. On your own machine that is usually what you want. In CI it means the result depends on whatever happens to be configured on the runner.

--bare skips that auto-discovery:

claude --bare -p "Summarize README.md" --allowedTools "Read"

Three things to know before you rely on it:

  • Bare mode never reads OAuth credentials or the system keychain. Set ANTHROPIC_API_KEY in the environment (or supply an apiKeyHelper through --settings), because your subscription login is not used.
  • Claude gets the Bash, file read and file edit tools. Anything else comes in through flags: --append-system-prompt for instructions, --settings for settings, --mcp-config for MCP servers, --agents for custom agents and --plugin-dir for a plugin.
  • There is a security reason too. Without --bare, a -p session runs the hooks in a project's .claude/settings.json and connects the servers in its .mcp.json with no trust dialog, even in a folder you have never opened before. Bare mode does not read them.

The documentation recommends --bare for scripted and SDK calls and says it will become the default for -p in a future release, so it is worth adopting now.

Get output a script can use

--output-format decides what lands on stdout:

FormatWhat you getUse it for
text (default)The answer as plain textPiping into a file or another command
jsonOne JSON object with result, session_id and metadataScripts that need the answer plus the session ID or cost
stream-jsonNewline-delimited JSON eventsLive progress in your own UI or logs

With jq, pulling out the text answer is one line:

claude -p "Summarize this project" --output-format json | jq -r '.result'

The JSON payload also includes total_cost_usd with a per-model breakdown. The docs describe both as client-side estimates that can differ from your actual bill, which is fine for spotting a runaway job but not for invoicing.

When you need a fixed shape rather than prose, add --json-schema. The structured answer appears in the structured_output field:

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'

An invalid schema now fails loudly with Error: --json-schema is not a valid JSON Schema, so a typo in CI breaks the build instead of silently returning free text.

For streaming, combine --output-format stream-json with --verbose and --include-partial-messages. Each line is an event, and the last line is a result message with the final text, cost and session metadata.

Three columns compare claude -p output formats: text prints the plain answer for piping into a file, json returns an object with result, session_id and total_cost_usd for jq, and stream-json emits system, stream_event and result lines for live progress.
--output-format text, json and stream-json return the same answer in three shapes; pick the one your script reads.

Approve tools without a prompt

A headless run has nobody to click "allow". You decide up front what Claude may do, in one of three ways.

List the tools. --allowedTools pre-approves specific tools, using permission rule syntax:

claude -p "Run the test suite and fix any failures" \
  --allowedTools "Bash,Read,Edit"

You can narrow Bash to command prefixes. The trailing space and asterisk matter: Bash(git diff *) matches commands starting with git diff, while Bash(git diff*) would also match git diff-index.

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 *)"

Pick a permission mode. --permission-mode sets a baseline for the whole run. acceptEdits lets Claude write files and run common filesystem commands such as mkdir and mv without asking. dontAsk denies everything that would otherwise prompt, which suits locked-down CI. auto has a classifier review most actions instead of you. The docs warn that a run with no mode set takes the built-in starting mode, which can be auto, so pass the one you mean.

Turn prompts off entirely. For scheduled jobs, --permission-prompts none (Claude Code v2.1.259 or later) denies anything that would fall back to a prompt, tells Claude not to retry it, and removes tools that need a person, such as AskUserQuestion:

claude -p "Update the dependency pins and run the tests" --permission-mode auto --permission-prompts none

With stream-json output, every denial shows up as a permission_denied event, and the final result lists them in permission_denials. Log that list: it tells you exactly which step needed a human.

Flow showing how claude -p decides a tool call such as Bash(npm test): first --allowedTools and allow rules, then the permission mode; if nothing matches, the call is denied and recorded in permission_denials. --permission-prompts none denies instead of waiting.
In a headless run nobody clicks allow: an allow rule or the permission mode approves a call, and anything else is denied and logged.

Continue a headless conversation

Each -p call is a full session with its own transcript, so you can pick it up again:

claude -p "Review this codebase for performance issues"
claude -p "Now focus on the database queries" --continue
claude -p "Generate a summary of all issues found" --continue

--continue resumes the most recent conversation. When several scripts run at once, "most recent" is a race, so capture the ID and resume that exact session instead:

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 Claude Code finds a session by ID from any directory on the machine. When you resume, the cost figure in the JSON output is the conversation's running total, earlier calls included.

Three recipes worth stealing

A typo linter in package.json. Piping the diff means Claude never needs Bash permission to read it:

{
  "scripts": {
    "lint:claude": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
  }
}

A security review of a pull request. --append-system-prompt adds instructions while keeping Claude Code's defaults:

gh pr diff "$1" | claude -p \
  --append-system-prompt "You are a security engineer. Review for vulnerabilities." \
  --output-format json

Stopping cleanly. If a supervisor sends SIGTERM, Claude Code exits with code 143, records no result for the unfinished turn and runs SessionEnd hooks. Send SIGINT instead if you want the turn to end properly first.

If you run these in GitHub, the Claude Code GitHub Action wraps the same agent loop and handles checkout, secrets and comments for you; our Claude Code GitHub Actions guide walks through the setup. For the Codex side of the same idea, see codex exec, and for automatic checks inside interactive sessions, see Claude Code hooks.

Where VibeiDE fits

Headless runs are great for one well defined job. When the jobs are coding tasks you want to watch and review, VibeiDE takes a different route: it is a desktop app that coordinates the Codex, Claude Code and OpenCode CLIs you already installed, with Claude Code running in an embedded terminal that shows the original CLI interface. Each project has its own task queue, each task keeps its saved provider conversation so a follow-up can continue it, and finished tasks wait in Ready for review, which is an unread completion state rather than a code approval. See Claude Code orchestration across your projects for how that looks in practice.

Takeaways

  • claude -p "prompt" is headless mode: one command, one answer, a real exit code, stdin in and stdout out.
  • Use --bare in CI and scripts so runs are reproducible and repository hooks or MCP servers do not load unasked. Set ANTHROPIC_API_KEY, because bare mode ignores your subscription login.
  • --output-format json gives you result, session_id and a cost estimate; --json-schema gives you structured_output in a fixed shape.
  • Decide permissions before the run with --allowedTools, --permission-mode or --permission-prompts none, and log permission_denials.
  • Capture session_id and use --resume instead of --continue whenever more than one run can happen at a time.
  • For long sessions you continue interactively, keep an eye on the context window; for parallel work, read run multiple Claude Code sessions.

Share this article

Post on XShare on LinkedIn

Related articles

Necessary cookies support sign-in, security, your language and this choice. Optional categories stay off until accepted.

First-party page views and acquisition measurement, plus Google Analytics 4 (Google Ireland, data may reach the US). Advertising personalization stays off.

With Analytics enabled, retain Google and Microsoft ad click identifiers and share download, signup, activation and purchase conversions with the relevant ad provider. No personalized advertising.

Privacy · Cookie inventory