Claude Code's non-interactive/headless mode runs a single prompt to completion without a human at the keyboard — the shape a CI pipeline needs, e.g. an automated PR review step. You invoke it with claude -p "<prompt>" (or --print); instead of dropping into the interactive REPL, Claude Code executes the task and exits, returning a non-zero exit code on failure so a pipeline step can branch on it. Requesting structured (JSON) output rather than free text makes the result something your pipeline script can parse and act on programmatically, the same structured-output discipline from Lesson 4.2 applied to Claude Code's own output.
Output formats: text, json, stream-json
The --output-format flag controls what headless mode returns. text (the default) prints the final response as plain text — fine for a human reading a log, fragile for a script. json returns a single JSON object once the run completes, including the final result, the session ID, and usage/cost data — this is what a CI script should parse to decide pass/fail. stream-json emits newline-delimited JSON events as the run progresses (tool calls, intermediate results, the final message) rather than waiting for completion, which matters for a long-running CI step where you want to stream progress into a log rather than see nothing until the very end.
Scoping tool access for an unattended run
A human is not present in CI to approve an unexpected permission prompt, so the permission mode for that run has to be decided in advance rather than negotiated interactively. --allowedTools and --disallowedTools let you pass an explicit tool allowlist/denylist on the command line for that invocation, scoping exactly what the run can touch (e.g. allow reading and running tests, disallow arbitrary shell). --permission-mode selects a broader policy: plan has Claude produce a plan without executing anything, acceptEdits auto-approves file edits but still gates other risky actions, and bypassPermissions skips permission checks entirely. That last one removes the safety net a human approval or a deny rule would normally provide, so it should only ever run inside an ephemeral, sandboxed CI container with no access to real credentials or production systems — never on a developer machine or a runner with broad access, since there is no human left to catch a destructive action before it happens.
Common exam distractor
Parsing Claude Code's free-text output with fragile string matching in a CI script is a trap; requesting structured JSON output (--output-format json) is the reliable way to make the result machine-actionable. A related trap: treating --dangerously-skip-permissions/bypassPermissions as simply a convenience for CI rather than a real security tradeoff that demands a sandboxed, isolated environment.
Session isolation between runs
Each CI run should start from a clean, isolated session — not accumulated context from a previous pipeline run — so results are reproducible and one run's review doesn't get contaminated by an unrelated earlier one. This is the CI-specific case of the same fresh-session principle from Lesson 7.1: a plain claude -p invocation without --resume or --continue already starts fresh, and CI pipelines should deliberately avoid those two flags rather than reach for them out of habit, since they resume a prior session's context, which is exactly what a reproducible pipeline run does not want.
Authentication: API key, not interactive login
Interactive claude login opens a browser for OAuth — there is no browser and no human to complete that flow on a CI runner. Headless CI instead authenticates via the ANTHROPIC_API_KEY environment variable, populated from the CI provider's secret store, never committed to the repo or hardcoded into a workflow file.
Wiring it into a pipeline
The official anthropics/claude-code-action GitHub Action wraps headless mode for the common case — triggering on an @claude mention in an issue or PR comment, or on a workflow event — and needs the repo permissions the task requires (e.g. contents: write, pull-requests: write) plus the API key as a repository secret. For anything more custom, a plain shell step invoking claude -p directly works on any CI provider:
- name: Claude review
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude -p "Review the diff in this PR for bugs and style issues. Output findings as JSON." \
--output-format json \
--allowedTools "Read,Grep" \
--max-turns 8 > review.json
node scripts/check-review.js review.json
--max-turns bounds how many agentic turns a single headless run can take, which matters in CI both for wall-clock time and for cost — an unattended run with no turn cap can, in principle, keep working far longer (and far more expensively) than intended before returning.