D2.5 and D3.4 governed permission inside an interactive session. This chapter takes the same agent out of the terminal and into a pipeline. The mechanics change — there is no one at the keyboard to approve a tool or answer a question — so a headless run has to decide its output shape and its permission surface up front. The payoff is that Claude Code becomes a scriptable, gated CI citizen.

The headless invocation

The entry point for everything in this chapter is one flag: “claude -p "<query>" is the canonical non-interactive invocation; the CLI exits after responding. All standard CLI options work with -p.” [Official] Run Claude Code programmatically · AnthropicT1-official original That single command runs the full agent loop and returns — no prompt, no session UI.

For CI you almost always pair it with --bare: “Add --bare to reduce startup time by skipping auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md. Without it, claude -p loads the same context an interactive session would, including anything configured in the working directory or ~/.claude. Bare mode is useful for CI and scripts where you need the same result on every machine.” [Official] Run Claude Code programmatically · AnthropicT1-official original It is the recommended mode for scripted calls and is slated to become the default for -p in a future release. [Official] Run Claude Code programmatically · AnthropicT1-official original

Output formats

A headless run can emit one of three shapes, selected with --output-format: text (default), json (a single payload with result, session_id, and total_cost_usd), and stream-json (newline-delimited events). [Official] Run Claude Code programmatically · AnthropicT1-official original The json form is what makes a run scriptable: “With --output-format json, the response payload includes total_cost_usd and a per-model cost breakdown, so scripted callers can track spend per invocation without consulting the usage dashboard.” [Official] Run Claude Code programmatically · AnthropicT1-official original

Structured output with --json-schema

When a downstream step needs a specific shape rather than prose, constrain the output to a schema: “To get output conforming to a specific schema, use --output-format json with --json-schema and a JSON Schema definition. The response includes metadata about the request (session ID, usage, etc.) with the structured output in the structured_output field.” [Official] Run Claude Code programmatically · AnthropicT1-official original The flag is --json-schema '<schema>'; the CLI reference describes it as producing “validated JSON output matching a JSON Schema after agent completes its workflow (print mode only).” [Official] CLI reference · AnthropicT1-official original

A minimal schema looks like this (illustration only — the shape is yours to define):

claude -p "Classify this PR's risk" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"severity":{"type":"string"}},"required":["severity"]}'

The schema-conforming result then arrives in structured_output, alongside the usual session_id and usage metadata.

Permission gates for a run with no human

The defining constraint of CI is that no one is there to approve a tool call, so the permission surface must be settled before the run starts. The locked-down mode from D3.4 is built for exactly this: --permission-mode dontAsk “denies anything not in permissions.allow or the read-only command set,” which the docs call out as useful for locked-down CI runs. [Official] Run Claude Code programmatically · AnthropicT1-official original Pair it with an allowlist: --allowedTools "Bash(git diff *),Read" auto-approves specific tools and supports prefix matching. [Official] Run Claude Code programmatically · AnthropicT1-official original

Two more knobs bound a run: --max-turns N “limits agentic turns and exits with error when reached,” and --max-budget-usd <N> caps dollar spend — both print-mode-only. [Official] CLI reference · AnthropicT1-official original At the far end, --permission-mode bypassPermissions (alias --dangerously-skip-permissions) skips prompts entirely [Official] Run Claude Code programmatically · AnthropicT1-official original — appropriate only inside an isolated container, never as a convenience.

Exit codes: what CI actually gates on

Output format decides what a run prints; the exit code decides whether the pipeline step passes. A CI step’s pass/fail is the exit status of the process it ran — so a headless claude -p that runs the full loop and “exits after responding” [Official] Run Claude Code programmatically · AnthropicT1-official original hands its exit code straight to the runner, and 0 means the step succeeds while non-zero fails it. The docs name concrete non-zero triggers you can rely on: --max-turns N “limits agentic turns and exits with error when reached,” [Official] CLI reference · AnthropicT1-official original and an over-cap stdin (10 MB) “returns a clear error and non-zero exit.” [Official] Run Claude Code programmatically · AnthropicT1-official original

The same mechanism gives you a clean pre-flight gate: claude auth status “exits 0 if logged in, 1 if not — useful as a CI gate before the agent step.” [Official] Run Claude Code programmatically · AnthropicT1-official original Run it first and the job fails fast with a clear cause instead of burning a turn on an unauthenticated agent call.

GitHub Actions and the credential model

The managed CI surface wraps all of the above. Claude Code GitHub Actions is “built on top of the Claude Agent SDK” [Official] Claude Code GitHub Actions · AnthropicT1-official original and wraps claude -p in a GitHub Action runner. Beyond the direct Anthropic API it supports two cloud providers — Amazon Bedrock (use_bedrock) and Google Vertex AI (use_vertex) — each authenticated through GitHub OIDC / Workload Identity Federation, so no static cloud keys are stored. [Official] Claude Code GitHub Actions · AnthropicT1-official original The v1.0 interface is deliberately small: “mode is auto-detected; use prompt for all instructions and claude_args for any CLI passthrough” [Official] Claude Code GitHub Actions · AnthropicT1-official original — so everything from earlier sections (--bare, --output-format, --allowedTools) reaches the runner through claude_args.

Practice

Exercise solutions

Solution ↑ Exercise

B. The job has no human to approve anything, so the permission surface must be fixed up front: dontAsk denies anything not pre-approved, --allowedTools "Read,Bash(npm test)" grants exactly the read and test-run capability (prefix matching scopes Bash to the test command), and --bare makes the run reproducible across machines. A does the opposite of locking down — --dangerously-skip-permissions approves everything, including edits and pushes. C auto-approves file edits, but the job is supposed to be read-only. D is the classic headless trap: in CI there is no one to answer the approval prompt, so a tool that falls through to default mode stalls or is denied rather than helpfully pausing. The locked-down combination is dontAsk + a tight allowlist + --bare.

Solution ↑ Exercise

Use --output-format json and read the session_id and total_cost_usd fields. The json form returns a single payload with result, session_id, and total_cost_usd (plus a per-model cost breakdown), so the later step can resume with the captured session_id and log spend from total_cost_usd — no usage-dashboard round-trip. The default text format returns only the final response (nothing parseable), and stream-json would make you reassemble the fields from an event stream.

Solution ↑ Exercise

The flag is --bare, and the cause it addresses is non-reproducible context discovery. Without --bare, claude -p auto-discovers and loads whatever the machine has — the repo’s CLAUDE.md, a developer’s personal ~/.claude config, locally-configured MCP servers, hooks, skills, plugins, auto memory — so the same command becomes a function of the host rather than of its inputs, and two runners diverge. --bare skips that discovery, making the run reproducible across machines (and it is slated to become the -p default).

Solution ↑ Exercise

(a) The job gates on the run’s process exit code: claude -p exits after responding and hands its exit status to the runner — 0 passes the step, non-zero fails it. (b) Two documented non-zero conditions: hitting --max-turns (“exits with error when reached”) and an over-cap stdin (piped input above the 10 MB limit “returns a clear error and non-zero exit”); claude auth status exiting 1 when not logged in is a third, useful as a pre-flight gate. (c) Swallowing the status — e.g. ending the command with || true or masking it behind a pipe — so the shell reports success even though the agent run failed; let the exit code propagate.

Exam essentials