← Alle Playbooks
Playbook· build

Claude Code headless in CI/CD, a setup that runs tomorrow

claude -p is the workhorse mode for scripting. We build a GitHub Actions workflow that checks tests on every PR and auto-fixes lint errors, without anyone watching.

Most people use Claude Code interactively. You type, it answers, the two of you do a little dance. It works. But at some point you want to run the same model automated, in a cron job, in GitHub Actions, as a pre-commit hook that checks something. That is exactly what the -p mode is for, sometimes called --print. A single request, one answer, exit. Scriptable, idempotent, CI-ready.

If you pull this all the way through, after 30 minutes you have a GitHub Actions workflow that runs a code review on every pull request and writes the result back as a comment. Plus two scripts for local cron jobs. Plus the understanding of why --bare is the difference between "on my machine" and "on every machine".

1. What claude -p actually is

claude -p "Deine frage" starts Claude Code, sends exactly this prompt, prints the answer to stdout, exits. No TUI, no live stream, no "Hi, what can I do". A shell command like any other.

claude -p "Was macht das auth-Modul?"

You can append everything that the interactive version also understands. --allowedTools "Read,Edit,Bash" so it gets going without a permission prompt. --output-format json if you want to pipe the result onward. --continue if the next request should continue in the same session. That is exactly what makes claude -p the workhorse for everything automated.

One thing many overlook: without further flags claude -p loads the complete context that an interactive session would also load. Hooks from ~/.claude/settings.json, MCP servers from .mcp.json, skills from ~/.claude/skills/, the CLAUDE.md in the working directory. That is handy on your machine. In CI it is a problem, because the machine there has no ~/.claude and the run behaves differently in every environment.

2. --bare is the CI switch

The trick for reproducible runs is --bare. With it, Claude skips auto-discovery: no hooks, no skills, no plugins, no MCP servers, no auto-memory, no CLAUDE.md. It takes only what you explicitly pass via a flag.

claude --bare -p "Fasse diese Datei zusammen" --allowedTools "Read"

Bare mode also has a faster start, because the whole discovery phase falls away. Authentication in bare mode runs via ANTHROPIC_API_KEY as an env var or via apiKeyHelper in the JSON that you pass to --settings. No OAuth, no keychain, nothing that only works locally.

Mnemonic for the rest of this playbook: local one-off scripts without --bare, everything that runs in CI with --bare.

3. Structured output with --output-format

In CI you normally don't want "a bit of prose". You want something to process further. There are three output formats:

  • text (default): the answer as plain text
  • json: a JSON object with result, session_id, token usage and metadata
  • stream-json: newline-delimited JSON, one event per line, for live streaming

For CI json is usually right. You get the answer plus all metadata in a structured block, can filter via jq, can reuse the session_id.

claude --bare -p "Extrahiere die Funktionsnamen aus auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}'

With --json-schema you force the answer into a shape. The result then sits in the structured_output field of the response. Anyone who ever had to parse LLM output with regex knows why that is valuable.

4. Permissions in headless mode

When claude -p gets a prompt that needs a tool, it would normally ask. In CI there is nobody to answer, the run would hang. Two ways to solve this.

First, tool whitelist. --allowedTools "Read,Edit,Bash" allows only these three tool categories. You can get finer with pattern matching: Bash(git diff *) allows any Bash command that starts with git diff . Watch the space before the star. Bash(git diff*) without a space would also match git diff-index, which is probably not intended.

Second, permission modes. --permission-mode acceptEdits lets Claude write files without asking and also accepts common FS commands like mkdir, touch, mv, cp automatically. --permission-mode dontAsk on the other hand blocks everything that is not in an allow rule or the read-only set, and aborts if Claude tries to do something else. For a locked-down CI, dontAsk with explicit allow rules is the safer setup.

5. The first real script run

Let's build something concrete. A script that pulls out changed files in your repo via git diff and asks Claude to check them for typos. Locally, no CI, just a Bash script.

#!/bin/bash
# review-staged.sh
set -euo pipefail

CHANGED=$(git diff --name-only --cached)
if [ -z "$CHANGED" ]; then
  echo "Keine staged Änderungen."
  exit 0
fi

claude -p "Pruefe diese geaenderten Dateien auf Tippfehler, unklare Variablennamen und fehlende Error-Handler. Sei kurz, gib mir nur die echten Funde mit file:line:" \
  --allowedTools "Read" \
  --output-format text

Make the script executable, drop it in as a pre-commit hook, done. The first real setup where you use claude -p daily without it feeling like "AI".

6. On to GitHub Actions

Now the whole thing in CI. A workflow that reviews the changed files on every PR and attaches the result as a comment to the PR.

In the repo under .github/workflows/claude-review.yml:

name: Claude PR Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Run review
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          DIFF=$(git diff origin/${{ github.base_ref }}...HEAD)
          echo "$DIFF" | claude --bare -p "Hier ist ein PR-Diff. Nenne die drei wichtigsten Issues, jeweils mit file:line. Falls alles ok ist, sag das in einem Satz." \
            --allowedTools "Read" \
            --output-format text > review.md

      - name: Post comment
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('review.md', 'utf8');
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body
            });

--bare is important here. Without it Claude would try to load ~/.claude, which does not exist on the GitHub runner. With --bare you get the same behavior on every machine.

7. Observe streaming output and retries

When the run takes longer you want to see what is happening. --output-format stream-json together with --verbose --include-partial-messages gives you one event per line. Filterable with jq.

claude --bare -p "Erklaere Rekursion" \
  --output-format stream-json --verbose --include-partial-messages | \
  jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'

What you also see in the stream are system events. On API errors Claude sends system/api_retry with attempt, max_retries, retry_delay_ms and an error category like rate_limit or server_error. That is gold for CI: you can build it into logs, can implement your own backoff, can pause the run differently on rate_limit than on authentication_failed.

On top of that comes system/init, the first event with session metadata. In it stands which model runs, which tools are available, which MCP servers were loaded, which plugins. If you want to make sure in CI that a certain plugin is loaded, parse the plugins array or the plugin_errors array and let the run fail if something is missing.

8. Context injection for CI runs

Bare mode loads nothing. That is clean, but sometimes you need context. Four flags help:

  • --append-system-prompt "Sei knapp" appends an instruction to the system prompt
  • --append-system-prompt-file path/to/style.md does the same from a file
  • --settings settings.json loads a settings file with tools, permissions, API helper
  • --mcp-config mcp.json loads MCP servers explicitly
  • --agents '{"reviewer": {...}}' defines sub-agents
  • --plugin-Dir ./my-plugin loads a local plugin folder

In CI you version all of this in the repo. A .claude-ci/style-guide.md for tone, a .claude-ci/mcp.json for the servers the run needs. On the run you point at it with flags, everything deterministic, everything in Git.

9. Cost and budget under control

In CI, runs happen without anyone watching. That also means: a bug or an endless loop can get expensive. Three safeguards I always build in.

First, small models where it works. For lint reviews and short diff checks Haiku is more than enough, it is a factor of 10 cheaper than Opus. With --model claude-haiku-4-5 you set this per run. For the PR review above that quickly saves 5 euros per day in a mid-sized repo.

Second, hard tool whitelist. The fewer tools allowed, the smaller the loop risk. If Claude in CI only needs Read, give it only Read. No Bash, no Edit, no web fetch.

Third, timeout in the CI job. GitHub Actions has timeout-minutes at the job level. Set it to something realistic plus a 50% buffer. If the run normally takes 4 minutes, set timeout-minutes: 8. On a loop the job snaps in time instead of eating hours.

10. What comes next

Once headless runs, three paths open up. First, parallel to the CI review, a local cron job that does an audit every night and writes the result as an issue into the repo. Second, multi-agent: one run calls claude -p with different --agents definitions, each agent checks one aspect, in the end a composite report. Third, the Python or TypeScript SDK instead of the CLI, in case you want more complex flows with tool-approval callbacks and native message objects.

For the first path read the recipe 5.2-schedule-routines. For the second 12.2-agent-research-orchestrate. For the third, the Agent SDK docs directly, where it goes beyond what is reachable via CLI.

Source

Specifics in this playbook (flags, output formats, events) verified against the official Anthropic documentation:

  • Headless mode + --bare + --output-format + stream events: https://code.claude.com/docs/en/headless
  • The examples for the PR review pipeline and the cost limiting are own practice, not doc quotes.