Running Claude Code headless (claude -p): agents in scripts, cron and CI
TL;DR
claude -p "prompt" runs one turn and exits, printing to stdout. Add --output-format json to parse it, --allowedTools to limit permissions and --max-turns to stop runaway loops. Great for scheduled reports, automated reviews and changelogs.
Claude Code isn’t only a chat tool in your terminal. Add the -p flag and it becomes an ordinary CLI command: take a prompt, do the work, print the result to stdout and exit — ready for scripts, cron jobs, Git hooks and CI pipelines. This guide goes from the basic command to a safe configuration for unattended runs.
The basic command
claude -p "What does the auth module do?"
-p (or --print) runs non-interactively. Claude Code exits 0 on success and non-zero on failure, so your scripts can branch on it. Failures during the run (e.g. missing authentication) are printed to stdout as the result.
Piping data in and out
# Explain a failed build log
cat build-error.txt | claude -p "Concisely explain the root cause of this build error" > output.txt
# Write a changelog from today's commits
git log --since=midnight --oneline | claude -p "Write a short changelog grouped by feature/fix"
Piped stdin is capped at 10 MB; for larger inputs write a file and reference it in the prompt.
--bare mode: use it for scripts and CI
By default claude -p auto-loads hooks, skills, subagents, plugins, MCP servers, auto memory and CLAUDE.md like an interactive session — even in folders you’ve never trusted, and without any trust dialog. Add --bare to skip all of that discovery:
claude --bare -p "Summarize README.md" --allowedTools "Read"
- Faster startup and identical results on every machine — a teammate’s hooks in
~/.claudeor MCP servers in the repo’s.mcp.jsonwon’t run. - No OAuth or keychain: set
ANTHROPIC_API_KEY(or Bedrock / Google Cloud / Foundry credentials). - Pass what you need explicitly:
--append-system-prompt,--settings,--mcp-config,--agents,--plugin-dir.
The official docs recommend --bare for all scripted and SDK calls and say it will become the default for -p in a future release.
Structured results
--output-format | Result |
|---|---|
text (default) | Plain text |
json | JSON with result, session_id, metadata, total_cost_usd |
stream-json | One JSON event per line, for real-time streaming |
# Just the text result
claude -p "Summarize this project" --output-format json | jq -r '.result'
# Force output into a JSON Schema
claude -p "List the main function names in auth.py" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'
Schema output lands in structured_output. If Claude declines or can’t produce valid output, the field is absent — your script must check for it. total_cost_usd lets you track each run’s cost (a client-side estimate).
Permissions — the most important part of unattended runs
Nobody is there to click “Allow”, so say up front what Claude may do:
# Allow only the git commands needed
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 *)"
--allowedToolsuses permission-rule syntax. A*after a space allows any command with that prefix —Bash(git diff *)differs fromBash(git diff*)(the latter also matchesgit diff-index).- Calls that need approval and aren’t allowed are denied (recent versions start
-pruns in Manual mode). --permission-modesets a baseline:dontAsk(deny anything that would prompt — good for locked-down CI),acceptEdits(file edits and basic file commands),auto(a classifier model approves instead of you).--permission-prompts nonefor unattended jobs: nothing waits for an answer, requests needing approval are denied, and Claude is told not to retry.
Customising the system prompt
# review.sh — security review of a PR
gh pr diff "$1" | claude -p \
--append-system-prompt "You are a security engineer. Review for vulnerabilities." \
--output-format json
--append-system-prompt adds instructions while keeping Claude Code’s default behaviour; --system-prompt replaces it entirely.
Multi-turn conversations
# First turn
claude -p "Review this codebase for performance issues"
# Continue the most recent conversation
claude -p "Now focus on the database queries" --continue
# Or keep the session_id to return to a specific session
sid=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$sid"
Real-world uses
| Task | Trigger | Suggested setup |
|---|---|---|
| Progress report from commits + tickets | Daily cron at 5 pm | --bare, Read + Bash(git log) only |
| PR review against the team checklist | GitHub Actions on PR open | Pipe gh pr diff, JSON output |
| Classify last night’s error logs | Cron at 7 am | --json-schema feeding a dashboard |
| Typo check on a diff | npm script | Pipe git diff main — no Bash permission needed |
| Release notes | On tag creation | Text output into a Markdown file |
Staying safe when automating
- Always declare a minimal
--allowedTools; avoid skipping permissions on machines with real data. - Use
--barein CI so an unfamiliar repo can’t run its own hooks or MCP servers. - Run in a dedicated container/runner with a dedicated automation API key and spend limit; cap each run with
--max-budget-usd. - Check the exit code and the presence of
structured_outputbefore processing further. - Need custom tools, permission callbacks or finer message handling → move to the Claude Agent SDK (Python/TypeScript).
How to apply it
- Developers: Add a
lint:claudescript that pipes your diff into Claude to catch typos and naming issues before opening a PR — no Bash permission required. - Tech Leads: Standardise one CI wrapper script: always
--bare, always an allowlist, always--output-format json, and logtotal_cost_usd. - PMs: Set up a 5 pm cron that turns commits and tickets into a daily report posted to the team channel — instead of asking “what did you do today” in stand-up.
Sources & references
- Run Claude Code programmatically — Claude Code Docs