Claude Code Headless Mode: Running It in Scripts and CI

Headless mode is Claude Code run with -p: one prompt in, one result out, no interface. What it loads, the three output formats, how to settle permissions before the run, and five examples you can put in a script or a pipeline.

7 min read

Claude Code headless mode means running claude with -p (or --print) and a prompt. It runs the task without the interactive interface, prints the result to standard output and exits with code 0 on success or non-zero on failure, so any script, Git hook or CI job can call it like another command-line tool. Choose the output with --output-format: text, json or stream-json. Because nobody is there to answer permission prompts, you decide what Claude may do before the run starts, with --allowedTools or a permission mode. In scripts and CI, add --bare so the run ignores whatever hooks, plugins and MCP servers happen to be configured on that machine.

What headless mode is, as documented today

Anthropic’s page for it still lives at the headless address, but it is now titled “Run Claude Code programmatically” (opens in a new tab), and it presents claude -p as the command-line form of the Claude Agent SDK. The same agent loop and tools are available as Python and TypeScript packages when a script outgrows shell. For anything you can express as a command, -p is enough.

Terminal
# Ask a question and print the answer
claude -p "What does the auth module do?"

# Pipe data in, write the answer out
cat build-error.txt | claude -p "Explain the root cause of this build error in three lines" > cause.txt

A few behaviors are worth knowing before you script it:

  • Without --bare, a -p run loads the same context an interactive session would: CLAUDE.md, hooks, skills, subagents, plugins and MCP servers from the project and ~/.claude.
  • It shows no workspace trust dialog. A project’s own .claude/settings.json hooks run and its .mcp.json servers connect, even in a folder you never trusted. Point it only at code you trust, or use --bare.
  • Piped standard input is capped at 10 MB. For larger inputs, write a file and name it in the prompt.
  • Skills and custom commands work: put /skill-name in the prompt. Commands that exist only in the terminal interface, such as /login, do not.
  • --bg, which starts a background session, cannot be combined with -p.

Bare mode for repeatable runs

--bare skips auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory and CLAUDE.md. Claude keeps Bash, file read and file edit tools, and you pass anything else explicitly: --append-system-prompt for instructions, --settings for settings, --mcp-config for servers, --agents for subagents. The documentation recommends it for scripted and SDK calls and says it will become the default for -p in a future release.

Bare mode never reads your subscription login, the system keychain or CLAUDE_CODE_OAUTH_TOKEN. Set ANTHROPIC_API_KEY, or supply an apiKeyHelper in --settings. Without --bare, a long-lived subscription token from claude setup-token works, as the authentication docs (opens in a new tab) describe.

The three output formats

  • text, the default: the answer as plain text. Right for a person or a file.
  • json: one JSON object at the end, with the answer in result, the session_id, usage and total_cost_usd. The cost is a client-side estimate and can differ from your bill.
  • stream-json: one JSON event per line as the run goes, ending with a result event. The documentation pairs it with --verbose; add --include-partial-messages to receive text as it is generated.

The stream is what a dashboard or a wrapper program reads. Its first event is normally system/init, which lists the model, tools, MCP servers and plugins that loaded, so a CI job can fail early if a server it needs did not connect. Retries show up as system/api_retry events. This filter, adapted from the documentation, prints only the text as it streams, using jq (opens in a new tab):

Terminal
claude -p "Summarize the open TODO comments in src/" \
  --output-format stream-json --verbose --include-partial-messages | \
  jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'

For answers that code will consume, --json-schema returns output validated against a schema in structured_output; that, and where transcripts go, is covered in Claude Code task output.

Permissions with nobody to ask

In -p runs, the starting permission mode is Manual, the ordinary prompting mode, and a prompt nobody answers is a denial. Settle it up front with the permission rule syntax (opens in a new tab) you already use in settings:

  • --allowedTools "Read,Edit,Bash(npm test *)" approves exactly those. The space before * matters: Bash(git diff *) matches git diff main, while Bash(git diff*) also matches git diff-index.
  • --permission-mode dontAsk denies anything that would have prompted. Reads in the working directory, read-only commands and whatever your allow rules cover still run. This suits locked-down CI.
  • --permission-mode acceptEdits lets Claude write files and run common filesystem commands such as mkdir and mv; other shell commands still need an allow rule.
  • --permission-mode auto has a classifier review each action instead of you.
  • --permission-prompts none, a recent flag, tells Claude that nobody can approve a request, so it stops retrying denied actions. Older versions reject it.

Ceilings on turns and spend (--max-turns, --max-budget-usd), timeouts and what an exit code 143 means are covered in how to automate Claude Code tasks.

Five examples

1. A typo linter in package.json

Piping the diff in means Claude needs no permission to run git at all. The escaped double quotes keep the script working on Windows.

package.json
{
  "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.\""
  }
}

2. A commit message from staged changes

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

Each pattern ends in * after a space, so Claude can read the diff and history and commit, but cannot push, reset or check out anything else.

3. The same question over many files

Terminal
for f in src/api/*.ts; do
  echo "## $f"
  claude --bare -p "List any endpoint in $f that reads user input without validating it. Reply NONE if there are none." \
    --allowedTools "Read"
done > validation-report.md

One short run per file keeps each context small and each answer about one file, and bare mode keeps a hook or plugin on this machine from changing the result. With only Read allowed, a surprising prompt can do no harm.

4. A conversation in several steps

Terminal
id=$(claude -p "Review src/checkout for performance issues" --output-format json | jq -r '.session_id')
claude -p "Now focus on the database queries" --resume "$id"
claude -p "Summarize every issue found as a checklist" --resume "$id" > checkout-review.md

Keeping the session_id lets later steps build on what the first one found without repeating it. --continue picks up the most recent conversation instead, which is simpler but unsafe when two scripts run at once.

5. A security review of a pull request

review.sh
#!/bin/bash
# usage: bash review.sh 123
gh pr diff "$1" | claude -p \
  --append-system-prompt "You are a security engineer. Review for vulnerabilities." \
  --output-format json | jq -r '.result'

Headless mode in CI

In GitHub, you rarely call claude -p yourself: the official action wraps it, handles authentication and posts results to issues and pull requests, as Claude Code GitHub Actions explains. Anthropic documents a GitLab CI/CD integration too. On any other runner, install Claude Code, set ANTHROPIC_API_KEY as a secret, and run claude --bare -p with an explicit tool list, a turn cap and a job timeout.

Two runtime details matter in pipelines. A background shell Claude starts, such as a dev server, is stopped about five seconds after the final result. A background subagent instead keeps the process open until it finishes, up to ten idle minutes by default. And --no-session-persistence keeps a CI run from saving a session to disk that nobody will resume.

Reporting the result to a board

A headless run’s answer lands in a file or a log. If the result belongs to a piece of work, put it on the task instead. fenbs connects over MCP, and a script cannot complete a browser sign-in, so issue a token by hand under Settings, “Connect an AI assistant”: give it a name, only the scopes it needs, and an expiry. Pass the server with --mcp-config, allow only the fenbs tools the job uses, such as mcp__fenbs__fenbs_comment, and end the prompt with the task’s ref: “Comment your findings on BUG-042.” The comment shows in the task’s history as the assistant acting for the person who issued the token, and revoking the token ends the job’s access.

Related

The full recipe for unattended runs: how to automate Claude Code tasks. Running jobs on a schedule: Claude Code scheduled tasks. Connecting a board by sign-in or token: the MCP docs.

Questions people ask.

What is Claude Code headless mode?

It is Claude Code run non-interactively with the -p or --print flag. It takes one prompt, works without the terminal interface, prints the result and exits with a status code, so scripts and CI jobs can call it. Anthropic now documents it as running Claude Code programmatically through the Agent SDK CLI.

How do I run Claude Code in headless mode?

Run claude -p followed by your prompt in quotes. Add --output-format json for machine-readable output, --allowedTools to approve the tools it may use without asking, and --bare in scripts so it skips locally configured hooks, plugins and MCP servers.

What output formats does Claude Code headless mode support?

Three: text, the default; json, a single object with the result, session ID, usage and an estimated cost; and stream-json, one JSON event per line as the run progresses, ending with a result event. The documented stream-json examples add --verbose.

Does bare mode use my Claude subscription?

No. Bare mode never reads OAuth credentials, the keychain or CLAUDE_CODE_OAUTH_TOKEN, so set ANTHROPIC_API_KEY or an apiKeyHelper. Without --bare, a long-lived token from claude setup-token authenticates with your subscription.

Start with one thing.

There is nothing to set up first. Write one line and you’ve started.