Running Claude Code Headless: claude -p in Scripts, Cron, and CI
Add -p and Claude Code stops being a chat and becomes a command: pipe input in, get text or JSON out, exit with a status code. How print mode works, how to control what it's allowed to do and spend, authentication for CI, and where headless runs still need somewhere to live.
Most people meet Claude Code as an interactive session: you type, it works, you watch. But the same agent can run without a human in the loop at all — from a shell script, a cron job, a git hook, or a CI pipeline. That's headless mode, and it's one flag: -p.
This guide covers how print mode works, the flags that make it safe to run unattended, how to authenticate it in CI, and the practical question of where these runs should happen.
Flags reflect Claude Code as of September 2026. Anthropic's headless docs and CLI reference are the source of truth.
The basics
claude -p "What does the auth module do?"
-p (or --print) runs the prompt, prints the result, and exits. No interface, no follow-up questions. It exits with status 0 on success and non-zero on failure, so scripts can branch on it like any other command.
It reads standard input, so it composes with pipes:
cat build-error.txt | claude -p "Explain the root cause of this build error" > diagnosis.txt
git diff main | claude -p "List any typos in this diff as file:line, nothing else"
Piping content in is often better than asking Claude to go and find it: the diff is already in the prompt, so the run needs no permission to execute git.
Getting structured output
Plain text is fine for humans. Scripts want JSON:
claude -p "Summarize this project" --output-format json | jq -r '.result'
The JSON includes the text result, the session_id, usage, and an estimated total_cost_usd. For output a program can rely on, add a JSON Schema and read structured_output:
claude -p "List every TODO comment in src/ with file and line" \
--output-format json \
--json-schema '{"type":"object","properties":{"todos":{"type":"array","items":{"type":"object","properties":{"file":{"type":"string"},"line":{"type":"integer"},"text":{"type":"string"}},"required":["file","line","text"]}}},"required":["todos"]}' \
| jq '.structured_output.todos'
For long runs where you want to see progress, --output-format stream-json emits one JSON event per line as the agent works.
Controlling what it's allowed to do
This is the part that matters. An interactive session can stop and ask before running a command. A headless run can't — there's nobody to ask. In -p mode, anything that would need approval and isn't pre-approved is denied, and the run carries on without it.
So you decide up front what the run may do.
Pre-approve specific tools with --allowedTools, using permission-rule syntax:
claude -p "Look at my staged changes and write a commit" \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"
The trailing * is a prefix match, and the space before it matters: Bash(git diff *) allows git diff --staged but not git diff-index.
Or set a baseline with a permission mode:
| Mode | In a headless run |
|---|---|
dontAsk |
Denies anything not explicitly allowed. The locked-down choice for CI. |
acceptEdits |
Writes files freely; shell commands still need allow rules. |
auto |
A classifier reviews each action instead of a human. |
bypassPermissions |
Everything runs. Only inside an environment that's disposable and isolated. |
claude -p "Apply the lint fixes" --permission-mode acceptEdits
The same reasoning from Running Claude Code Unattended applies here, more strongly: an unattended run with broad permissions is only as safe as the machine it runs on.
Limiting how much it can do
Two flags stop a run from wandering or running up a bill:
--max-turns 10— caps the number of agent turns, and exits with an error when the cap is hit.--max-budget-usd 2.00— stops once estimated spend reaches the cap.
Set both on anything that runs on a schedule. A prompt that works fine by hand can loop on an unexpected input at 3am, and nobody's watching.
Reproducible runs with --bare
By default, claude -p loads everything an interactive session would: CLAUDE.md, hooks, skills, MCP servers from .mcp.json, and whatever's in ~/.claude. That means the same command behaves differently on your laptop and your teammate's.
--bare skips all of that auto-discovery. You pass exactly the context you want with flags — --append-system-prompt, --settings, --mcp-config — and the run behaves the same everywhere. Anthropic recommends it for scripted use and has said it will become the default for -p.
It also matters for safety. Without --bare, a -p run in a repository you've just cloned runs that repository's hooks and connects its MCP servers, with no trust prompt. If you're running headless Claude over code you didn't write, use --bare.
One catch: bare mode doesn't read your subscription login. It needs ANTHROPIC_API_KEY set.
Multi-step jobs: continuing a conversation
Each -p call is a new conversation unless you tell it otherwise. To build on the previous one:
claude -p "Review this codebase for performance issues"
claude -p "Now focus on the database queries" --continue
For scripts that run several conversations at once, capture the session ID and resume that one specifically:
id=$(claude -p "Start a dependency audit" --output-format json | jq -r '.session_id')
claude -p "Now propose upgrade PRs for the three riskiest" --resume "$id"
Authentication in CI
A CI runner can't open a browser to log in. Two options:
- An API key from the Claude Console, set as
ANTHROPIC_API_KEY. Billed per token. The right choice for shared, organisation-level automation. - A long-lived token for your subscription, generated locally with
claude setup-tokenand stored asCLAUDE_CODE_OAUTH_TOKEN. Runs count against your Pro or Max plan instead of API billing. It's tied to the person who created it, so it suits personal automation more than team pipelines.
Store either one as a CI secret, never in the repository. Which billing model suits how much you automate is its own question — see Claude Pro vs Max vs API Key for Claude Code.
GitHub Actions
For GitHub specifically, Anthropic maintains an action, anthropics/claude-code-action, built on the same engine. Run /install-github-app inside Claude Code to set it up, or add the workflow yourself. It has two modes:
- Interactive: mention
@claudein an issue or pull request comment and it responds there — implementing a change, answering a question, fixing a failing test. - Automation: give it a
promptinput and it runs on any GitHub event, including a cron schedule.
CLI flags pass through a claude_args input, so --max-turns, --allowedTools, and --model all work the same way.
Useful things to automate
Some patterns that earn their keep:
- PR review for a specific concern — security, migrations, accessibility — piping
gh pr diffin with a focused--append-system-prompt. - Nightly triage — summarise yesterday's error logs and open issues into a short report.
- Dependency upkeep — bump pins, run the tests, open a PR if they pass.
- Release notes from the commit log since the last tag.
- A project-specific linter in
package.jsonscripts, for rules too fuzzy for a regex.
Start with read-only jobs (reports, reviews) before letting a headless run change code. Once it does, have it open a pull request rather than pushing to main.
Where headless runs should live
Here's the question most guides skip. A headless run needs a machine, and the choice shapes what it can do:
- Your laptop — only runs when the laptop is awake. Fine for git hooks and one-off scripts, useless for a 3am cron job.
- A CI runner — clean and disposable, which is great for reproducibility, but it starts from nothing each time. Every run reinstalls dependencies, has no database with real-shaped data, and forgets everything when it exits. Minutes are billed per run.
- A persistent server or workspace — the environment survives between runs: dependencies installed, database migrated, previous results on disk. A nightly job can pick up where last night's left off, and verify its work against a running app rather than a fresh checkout.
Ephemeral runners suit checks that should start clean. Persistent environments suit agents doing ongoing work. Most teams end up wanting both — the argument for the second is in Why AI Coding Agents Need Persistent Workspaces.
A safe default command
For a scheduled headless job, a good starting point looks like this:
claude --bare -p "$(cat prompts/nightly-triage.md)" \
--permission-mode dontAsk \
--allowedTools "Read,Bash(npm test *)" \
--max-turns 15 \
--max-budget-usd 3 \
--output-format json > "reports/$(date +%F).json"
Reproducible context, nothing runs unless allowed, capped turns and spend, machine-readable output. Loosen it deliberately, one permission at a time.
EasySpawn gives Claude Code a persistent, isolated workspace that's always on — so headless jobs run against an environment that already has your dependencies, your database, and your running app, with you connecting in from any device when you want to watch. See how it works or join the waitlist.
Related: Claude Code Hooks: Rules the Agent Can't Forget · How to Run Claude Code on a Remote Server · Claude Code Subagents · What Is JSON? · Evals for Coding Agents
Keep reading
Running Claude Code Agents in Parallel With Git Worktrees
Two agents in one checkout will overwrite each other's work. Git worktrees give each Claude Code session its own files and branch on the same repository. How to set it up, and the parts nobody warns you about: ports, databases, and dependencies.
Claude Code Subagents: When to Split the Work (and When Not To)
Subagents give Claude Code a second context window: a helper that does a noisy job — running tests, searching a codebase, reading logs — and hands back only the summary. How they work, how to write your own, and the tasks where they help versus the ones where they just add cost.