Claude Code Headless: Scripting, CI, and Cron Automation
Claude Code headless mode runs Claude as a scriptable CLI process for CI pipelines, cron jobs, and pipes, no terminal UI required.
Who this is for: developers and technical founders who already use Claude Code interactively and want to wire it into CI, cron, or a shell pipeline without babysitting a chat window.
Claude Code headless mode is the -p (print) flag on the CLI: instead of opening the interactive TUI, claude -p "your prompt" runs once, streams or prints its output, and exits with a status code. That single detail turns Claude Code from a pair-programming tool into a process you can call from a bash script, a GitHub Actions job, or a cron entry. This guide covers how headless mode actually works, the gotchas that trip people up in production, and three real automation patterns you can copy today.
What headless mode actually changes
Interactive Claude Code assumes a human is at the keyboard: it asks for permission before editing files, waits for you to approve tool calls, and keeps a persistent session open. Headless mode strips all of that away by design.
claude -p "prompt"runs the prompt once and exits. No REPL, no follow-up questions.- Permission prompts either get auto-approved (via
--dangerously-skip-permissionsor a pre-configuredsettings.jsonallowlist) or the run fails silently waiting for input that will never come. This is the single most common cause of a CI job that hangs until it times out. - Output can be plain text, or structured JSON with
--output-format json, which is what you want the moment another program needs to parse the result. - Session state does not persist between invocations unless you explicitly pass
--resume <session-id>or pipe conversation history back in.
Warning: if you runclaude -pin CI without configuring permissions first, the process will not crash cleanly, it will wait for a TTY prompt that CI can't answer. Always test the exact command locally with
Basic syntax and flags worth knowing
# Simplest form: prompt in, text out, exit 0/1
claude -p "Summarize the diff in the last commit"
# Structured output for programmatic consumption
claude -p "List all TODO comments in src/" --output-format json
# Skip permission prompts entirely (only in sandboxed/CI environments)
claude -p "Run the test suite and report failures" --dangerously-skip-permissions
# Cap turns so a misbehaving agent can't loop forever
claude -p "Refactor the auth module" --max-turns 8
Three flags matter most for automation:
--output-format jsonwraps the response in a JSON envelope with fields likeresult,session_id,total_cost_usd, andnum_turns. Parse this withjqrather than scraping text.--max-turns Nis your circuit breaker. Without it, an agentic task that gets stuck in a tool-call loop will keep burning tokens until it either succeeds or times out at the CI job level, and CI timeouts are usually 10-60x longer than they need to be for a bounded task.--allowedTools(or the project's.claude/settings.jsonpermissions block) lets you grant exactly the tool access a headless run needs, e.g.Bash(git *)andReadbut notWrite, without disabling permission checks globally.
Piping and composing with Unix tools
The real value of headless mode is that Claude Code becomes a normal citizen in a shell pipeline. It reads stdin, writes stdout, and respects exit codes, so it composes the way grep or jq does.
# Feed a file's content in, get a fix out, write it back
cat src/utils/parser.js | claude -p "Fix the off-by-one bug in this parser, output only the corrected file" > src/utils/parser.js.new
# Chain into git
git diff --staged | claude -p "Write a conventional commit message for this diff" | git commit -F -
# Filter JSON output with jq
claude -p "Audit package.json for outdated deps" --output-format json | jq -r '.result'
Tip: when piping Claude's output directly into a file or another command, always write to a .new or temp path first and diff it before overwriting the original. A hallucinated edit that silently replaces a working file is the most expensive mistake in this whole workflow.
CI pipeline pattern: GitHub Actions
A common use: run Claude Code as a PR reviewer or changelog generator on every push.
name: claude-review
on: [pull_request]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Claude Code
run: npm install -g @anthropic-ai/claude-code
- name: Run headless review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
git diff origin/main...HEAD > /tmp/diff.txt
claude -p "Review this diff for bugs and security issues. Be concise: $(cat /tmp/diff.txt)" \
--output-format json \
--dangerously-skip-permissions > /tmp/review.json
jq -r '.result' /tmp/review.json
Two things to get right here that people usually skip:
- Pin the Claude Code version in CI (
npm install -g @anthropic-ai/[email protected]). A silent minor upgrade changing default behavior mid-pipeline is a bad debugging afternoon. - Set a hard job timeout (
timeout-minutes: 5at the job level) as a second circuit breaker on top of--max-turns. Belt and suspenders, because CI minutes cost money and a stuck agent will happily run until the platform default (usually 360 minutes) kills it.
Cron and scheduled agent pattern
For recurring tasks, a plain cron entry that calls a wrapper script is more reliable than a long-running daemon.
#!/usr/bin/env bash
# /opt/scripts/nightly-audit.sh
set -euo pipefail
cd /opt/repos/my-app
LOG="/var/log/claude-nightly-$(date +%F).log"
claude -p "Check for any TODO or FIXME older than 30 days in git blame, list them with the responsible commit" \
--output-format json \
--allowedTools "Bash(git *)" "Read" \
> "$LOG" 2>&1
if [ $? -ne 0 ]; then
# forward failure to your alerting channel
curl -s -X POST "$SLACK_WEBHOOK" -d "{\"text\":\"Nightly Claude audit failed, see $LOG\"}"
fi
# crontab -e
0 3 * * * /opt/scripts/nightly-audit.sh
Cron gives you no interactivity by definition, so this is the pattern where the permission-prompt trap bites hardest. Test the exact script manually with bash -x before trusting cron to run it unattended, and always redirect both stdout and stderr to a log file, since cron mails failures nowhere useful by default on most modern setups.
JSON output and structured parsing
--output-format json is what makes headless Claude Code usable as a function call from another program rather than a chat transcript. The envelope looks roughly like this:
{
"result": "Found 3 unused exports in src/lib/",
"session_id": "8f2a...",
"num_turns": 4,
"total_cost_usd": 0.0231,
"duration_ms": 5820
}
Two fields worth watching in production: total_cost_usd (log it, aggregate it, alert if a single run spikes) and num_turns (a proxy for how much the agent had to explore before answering; if it's consistently near your --max-turns ceiling, your prompt needs more context up front, not a higher ceiling). For deeper coverage of designing the JSON contract itself, see structured outputs and, if you're chaining tool calls inside the prompt, tool calling patterns.
Common gotchas
| Gotcha | Symptom | Fix |
|---|---|---|
| No permission pre-approval | Job hangs until CI timeout | Use --dangerously-skip-permissions in a sandboxed runner, or pre-approve tools in .claude/settings.json |
No --max-turns cap | Runaway token spend on a stuck loop | Set --max-turns 6-10 for bounded tasks |
| Parsing plain text output | Brittle regex breaks on model phrasing changes | Always use --output-format json for anything downstream reads |
Missing ANTHROPIC_API_KEY in CI env | Auth error buried in generic failure | Explicitly check the env var is set before the claude call |
| Assuming session persists across cron runs | Each run has no memory of prior runs | Pass --resume <session-id> explicitly if continuity is required, or feed prior state via the prompt itself |
| Writing agent output straight over source files | Silent corruption if the model misreads context | Diff against a temp file first, or run inside a git worktree you can discard |
For the version of this problem where the pipeline is a full autonomous agent rather than a single scripted call, Claude Agent SDK gives you more control over the loop than shelling out to the CLI. And if permission handling above sounds unfamiliar, read Claude Code permissions and safety before you flip --dangerously-skip-permissions on anything with write access to production.
Cost control at scale
Headless runs add up fast once they're on a schedule. A nightly audit at $0.02-0.05 per run is nothing; the same prompt firing on every push to a busy monorepo, multiplied by 40 PRs a day, is a real line item. Track total_cost_usd from the JSON output into a simple log or dashboard from day one, not after the first surprising invoice. See Claude Code token cost control for concrete levers: prompt caching, model selection per task, and trimming context before it hits the API.
When not to use headless mode
Headless mode is the wrong tool for open-ended exploration, first-pass debugging of something you don't understand yet, or any task where you'd want to interrupt and redirect mid-stream. It's the right tool for well-scoped, repeatable tasks with a clear success condition: run the linter and summarize, generate a commit message, check a diff against a rule, regenerate a changelog. If you're still deciding whether an agentic approach fits your task at all, vibe coding: when to use it is a useful gut check before you automate anything.
FAQ
What is Claude Code headless mode?
Headless mode is Claude Code run with the -p (print) flag, which executes a single prompt non-interactively and exits, instead of opening the interactive terminal UI. It's designed for scripts, CI jobs, and cron, where no human is present to approve tool calls or read a live transcript.
How do I stop Claude Code from hanging in CI?
The hang almost always comes from an unanswered permission prompt. Either pass --dangerously-skip-permissions in a sandboxed CI runner, or pre-configure an allowlist of tools in .claude/settings.json so the run never needs to ask. Pair that with --max-turns and a job-level timeout as backups.
Can Claude Code output structured JSON for scripts to parse?
Yes, pass --output-format json and parse the result field with jq or your language's JSON parser. This is more reliable than parsing plain text output, which can vary in phrasing between runs.
Does a headless Claude Code run remember previous runs?
No, by default each claude -p invocation is a fresh session with no memory of prior calls. Use --resume <session-id> if you need continuity, or explicitly feed relevant prior state (a log excerpt, a prior result) into the new prompt.
Is it safe to run claude -p on a cron job unattended?
Yes, if you scope its tool permissions tightly (--allowedTools), cap turns, redirect stdout/stderr to a log file, and add alerting on non-zero exit codes. Treat it like any other unattended script: least privilege, logging, and a failure notification path.
How much does a headless Claude Code run cost?
It's billed the same as any Claude API usage, based on input/output tokens for the model you select, and the JSON output's total_cost_usd field reports it per run. A short, well-scoped task with a fast model often costs a fraction of a cent; log this field over time rather than guessing.
Sources
- Claude Code documentation
- Claude Code CLI reference
- Anthropic API pricing
- GitHub Actions documentation
Want this wired into your own repo's CI or a nightly cron job without the trial and error? Book a free 30-minute call via the contact form or message us directly on WhatsApp.