#4: Headless Mode 👻
Today we'll look at running Claude Code without the interactive UI, which turns it into a building block for scripts, CI, and cron jobs.
The -p flag (short for --print) is the key. It sends one prompt, lets Claude work, prints the result, and exits. The same engine powers the Agent SDK, so anything we learn here carries over.
Let's take a look.
The Basics
At its simplest, we pass a prompt and read the answer:
claude -p "Explain what this project does"It reads stdin too, so it fits right into a pipeline:
git diff HEAD~5 | claude -p "Summarize these changes" > changelog.txtFor scripts and CI, the docs recommend adding --bare. It skips discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md, so startup is faster and the run doesn't depend on whatever happens to be configured on the machine:
claude --bare -p "Summarize README.md" --allowedTools "Read"Output Formats
The --output-format flag picks what comes back:
- text: just the final answer (the default)
- json: the answer plus metadata like
session_id, usage, andtotal_cost_usd - stream-json: one JSON event per line while Claude works
claude -p "Analyze this codebase" --output-format json | jq -r '.result'For structured data, --json-schema makes the answer match a schema, and it shows up in a structured_output field:
claude -p "List the exported functions in src/hal.ts" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'Neat! If Claude declines or the run fails, that field is missing, so it's worth checking for it before using it.
Turn And Budget Limits
Unattended runs need guardrails:
claude -p "Review this PR for bugs" \
--max-turns 5 \
--max-budget-usd 1.00 \
--fallback-model sonnet,haiku--max-turns caps the agentic turns and exits with an error when it hits the limit. --max-budget-usd stops the run once estimated spend reaches the cap (it's a client-side estimate, so leave some headroom). --fallback-model takes a list of models to try if the main one is overloaded.
Permissions
There's nobody around to click "Allow" in a script. --allowedTools pre-approves exactly what the job needs, using permission rule syntax:
claude -p "Look at the staged changes and write a commit" \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"The space before * matters. Bash(git diff *) matches git diff --cached but not git diff-index.
For a locked-down CI run, --permission-mode dontAsk denies anything that would otherwise prompt, while still allowing reads and whatever our allow rules cover.
In GitHub Actions
Anthropic publishes an official action, so we don't have to install the CLI by hand:
name: Claude review
on: [pull_request]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "Review this pull request for bugs and security issues"Continuing Conversations
Multi-step scripts can pick a conversation back up:
# Save the session ID from the first run
session_id=$(claude -p "Start a review of src/" --output-format json | jq -r '.session_id')
# Continue that exact conversation
claude -p "Now focus on the database queries" --resume "$session_id"
# Or continue the most recent one
claude -p "Summarize everything we found" --continueWatch Out
Without --bare, a -p run in a project folder runs that project's hooks and connects its .mcp.json servers, even in a folder we've never trusted, and there's no trust dialog to stop it. On CI or with code we didn't write, --bare is the safer default.
To add instructions for one script without touching any settings, there's --append-system-prompt.
More info about running Claude Code from scripts can be found at the Claude Code docs.