Claude Code Learning Hub
中文 Mingyu's Library

Hub / Course / D13

D13 · Headless & CI

Turn Claude Code from "a terminal session you watch" into "one command in a pipeline": headless claude -p runs, structured output, unattended permission lockdown, and how to wire in the official GitHub Action.

For the first twelve lessons, Claude Code always had a person sitting at the terminal making the calls; this lesson removes that person. You'll learn headless mode's (claude -p) input/output contract — prompt in, text or JSON out, exit code decides success — what happens to permissions when nobody is there to click "Allow", and finally how to wire it into your PR flow with the official GitHub Action.

Why this lesson comes today

This lesson is the "unattended edition" final assembly of several earlier ones: in D3's permission modes you learned interactive allow/deny, but in CI nobody answers prompts — the answers must be written in advance into --allowedTools or a permission mode; and the "machine referee" idea from D8's Hooks holds in CI too — in headless runs, hooks are one of the few mechanisms that can still forcibly block an action.Our take

Looking ahead, D14 (Plugins) covers packaging and distribution, and headless is one of the main places distributed plugins get consumed: the official action supports installing plugins and running their skills in CI. After this lesson, read the hands-on tips F3 · GitHub Actions integration and F4 · Scheduled tasks — that's the step from "understanding the mechanism" to "copying a working setup".

Core concepts, explained

headless: the input/output contract of a single command

OfficialAdd -p (i.e. --print) to any claude command and it becomes non-interactive: execute the prompt, print the result, exit; every CLI flag can be combined with -p. It reads stdin, so it takes pipes like any ordinary Unix tool (piped stdin capped at 10MB since v2.1.128):

# Q&A style: result goes to stdout
claude -p "What does the auth module do?"

# Pipe style: build log in, explanation out
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt

OfficialSuccess is expressed through exit codes: 0 on success, non-zero on failure, so scripts can branch directly on exit status; on SIGTERM it aborts the current turn, runs SessionEnd hooks, and exits with 143. OfficialIn CI, add --bare: it skips hooks, skills, plugins, MCP, auto memory, and CLAUDE.md auto-discovery, guaranteeing identical results on every machine and faster startup; the docs note it will become the default behavior of -p in the future. Note that bare mode doesn't read OAuth credentials or the system keychain — you must authenticate via the ANTHROPIC_API_KEY environment variable (or apiKeyHelper in settings).

Three output formats: for humans vs for programs

--output-formatOutputBest for
text (default)Plain-text answerHumans, simple pipes
jsonOne JSON object: result, session_id, total_cost_usd, and per-model costsScript parsing, per-run accounting
stream-jsonLine-by-line JSON event stream (with --verbose --include-partial-messages you get token-level increments)Live progress, building your own UI

OfficialFor structured output conforming to a fixed schema, use --output-format json with --json-schema; the result lands in the structured_output field, ready to pluck values from with jq; an invalid schema errors out (since v2.1.205). OfficialThe first event of the stream-json stream, system/init, carries fields like mcp_server_errors and plugin_errors — CI can fail directly on "non-empty error array", catching the silent failure where a server didn't load but the process still exited cleanly.

Unattended runs: what happens to permissions

This is the CI answer to D3's question. In an interactive session a human answers permission prompts; in headless there's nobody there, so the docs give three layers of lockdown:Official

  • Allow item by item: --allowedTools "Bash(git diff *),Read,Edit", using permission-rule syntax precise down to the command prefix. Mind the space before the *: Bash(git diff *) only allows commands starting with git diff; writing git diff* would let git diff-index through as well.
  • Set a baseline: --permission-mode dontAsk denies everything not covered by allow rules or the read-only command set — right for locked-down CI; --permission-mode acceptEdits writes files without prompting and auto-allows common filesystem commands like mkdir, touch, mv, cp, while other shell commands still need allow rules or the run aborts.
  • Cap the damage: --max-turns limits turns (the CLI has no default limit; hitting it errors out), --max-budget-usd caps dollar spend per run (subagent spend counts too; enforcement of the cap requires v2.1.217+).
Common misconception"Nobody's watching in CI, so just open everything with --dangerously-skip-permissions" — convenient, but it hands the model's output the full permissions of your repo and runner, and if the prompt gets injected via PR content there is no gate left at all. The sturdier CI combination: precise allowedTools + a dontAsk/acceptEdits baseline + max-turns/budget caps, with D8's hooks as hard blocks on top.Our take

Wiring into GitHub Actions

OfficialThe official GitHub Action is anthropics/claude-code-action@v1 (under the hood it's the Agent SDK / headless capability). It auto-detects its mode: configured with event triggers but no prompt, it responds to @claude mentions in comments (interactive mode); given a prompt input, it executes directly (automation mode). Any CLI argument passes through via claude_args — for example --max-turns (defaults to 10 in the action), --model, --allowedTools.

@claude mention pull_request event schedule (cron) GitHub Actions runner (CI) claude-code-action@v1 runs claude -p headless Auth: secrets.ANTHROPIC_API_KEY injected; permissions locked down via claude_args PR comments / reviews New commits / PRs JSON results & exit codes
Three trigger types feed the same runner: the official action runs Claude headless and produces comments, commits, or JSON for downstream steps to consume.

OfficialAuth: the fastest path is running /install-github-app in a Claude Code terminal (requires repo admin; the GitHub App needs read/write on Contents/Issues/Pull requests; the shortcut is only for users connecting directly to the Claude API). The manual path: install the App, store the API key in the repo secret ANTHROPIC_API_KEY, and copy a workflow from the official examples. Subscribers can generate a long-lived OAuth token for CI with claude setup-token. Enterprise environments can go through Amazon Bedrock / Google Cloud, using OIDC instead of storing static keys, with use_bedrock or use_vertex enabled on the action side. Never put the key in the workflow file.

OfficialCost: CI spends two currencies — GitHub Actions minutes + API tokens. The official controls: specific, well-scoped @claude instructions to cut wasted invocations, --max-turns against endless iteration, workflow-level timeouts against runaway jobs, and GitHub concurrency to limit parallelism. Combine with total_cost_usd from --output-format json for per-run accounting.

Hands-on: doable today

The first three steps only need Claude Code installed locally, about 15 minutes; step 4 needs a GitHub repo where you have admin rights.

  1. Run a minimal headless call and check the exit code. In any project directory run the first command below. Expected: a JSON object containing result (the answer text), session_id, and total_cost_usd; right after, echo $? should print 0.
  2. Use it as a Unix tool. Run the second, piped command. Expected: Claude outputs only the spelling problems in the diff (no pleasantries) — the embryo of "Claude as a linter". It can read the diff without any Bash permission because the content arrives through the pipe.
  3. Feel the permission lockdown. The third command runs a file-modifying task under the acceptEdits baseline; expected: no confirmation prompts at all. Then remove --permission-mode acceptEdits and re-run; expected: the task can't finish because nobody is there to approve the file writes — which is exactly why CI requires permissions declared up front.
# 1. Minimal headless call + exit code
claude -p "Summarize what this project does in one sentence" --output-format json
echo $?

# 2. Pipe: diff in, spell check out
git diff main | claude -p "You are a spell checker. For every spelling mistake in the diff, print one line with file:line, then the problem on the next line. No other output."

# 3. Permission lockdown: run with the baseline, then remove it and compare
claude -p "Fix the typos in README.md" --permission-mode acceptEdits --max-turns 5
  1. Wire into GitHub Actions. In a repo where you have admin rights, open interactive Claude Code and run /install-github-app; follow the guide to install the App, add the secret, and generate the workflow. Then comment on any issue or PR with @claude Where are the weak spots in this repo's test coverage? — expected: Claude replies as a comment within a few minutes. No suitable repo? Read and understand the minimal workflow below today, and install it tomorrow:
name: Claude Code
on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
jobs:
  claude:
    runs-on: ubuntu-latest
    steps:
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          claude_args: "--max-turns 10"

How to know you've learned it

  • You can explain claude -p's input/output contract to someone else: prompt/stdin in, text/json/stream-json out, exit code 0 or non-zero decides success.
  • You can say which scenarios json and stream-json each fit, and which field --json-schema results land in.
  • Hands-on steps 1–3 all ran, and you can explain why the behavior changed in step 3 after removing the permission baseline.
  • You can name the three CI auth routes: the ANTHROPIC_API_KEY secret, claude setup-token for subscribers, and OIDC via Bedrock/Vertex.
  • Self-test: in CI you want Claude to fix lint and commit, unattended — no prompts, but without opening up all permissions either. How do you configure it? (Key points: --allowedTools precisely allowing the git/lint commands + an acceptEdits or dontAsk baseline + --max-turns/--max-budget-usd as stop-loss; not --dangerously-skip-permissions.)