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.
# 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-prun 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.jsonhooks run and its.mcp.jsonservers 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-namein 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 inresult, thesession_id, usage andtotal_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 aresultevent. The documentation pairs it with--verbose; add--include-partial-messagesto 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):
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 *)matchesgit diff main, whileBash(git diff*)also matchesgit diff-index.--permission-mode dontAskdenies 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 acceptEditslets Claude write files and run common filesystem commands such asmkdirandmv; other shell commands still need an allow rule.--permission-mode autohas 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.
{
"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
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
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.mdOne 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
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
#!/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.