Claude Code headless in CI/CD, ein Setup das morgen schon läuft
claude -p ist der Workhorse-Modus fuers Skripten. Wir bauen einen GitHub-Actions-Workflow der bei jedem PR Tests prüft und Lint-Fehler auto-fixt, ohne dass jemand zuschauen muss.
Die meisten benutzen Claude Code interaktiv. Du tippst, er antwortet, ihr habt einen kleinen Tanz. Funktioniert. Aber irgendwann willst Du das gleiche Modell automatisiert laufen lassen, in einem Cron-Job, in GitHub Actions, als Pre-Commit-Hook der was prüft. Genau dafuer gibt es den -p Modus, manchmal --print genannt. Eine einzelne Anfrage, eine Antwort, Exit. Skriptbar, idempotent, CI-ready.
Wenn Du das hier komplett durchziehst, hast Du nach 30 Minuten einen GitHub-Actions-Workflow der bei jedem Pull Request einen Code-Review fahrt und das Ergebnis als Kommentar reinschreibt. Plus zwei Skripte für lokale Cron-Jobs. Plus das Verstaendnis warum --bare der Unterschied zwischen "auf meiner Maschine" und "auf jeder Maschine" ist.
1. Was claude -p eigentlich ist
claude -p "Deine frage" startet Claude Code, schickt genau diesen Prompt, druckt die Antwort auf stdout, exitet. Kein TUI, kein Live-Stream, kein "Hi, was kann ich tun". Ein Shell-Befehl wie jeder andere.
claude -p "Was macht das auth-Modul?"
Du kannst alles dranhaengen was die interaktive Version auch versteht. --allowedTools "Read,Edit,Bash" damit er ohne Permission-Prompt loslegt. --output-format json wenn Du das Ergebnis weiterpipen willst. --continue wenn die naechste Anfrage in derselben Session weitergehen soll. Genau das macht claude -p zum Workhorse für alles automatisierte.
Eine Sache die viele ubersehen: ohne weitere Flags laedt claude -p den kompletten Context den eine interaktive Session auch laden wuerde. Hooks aus ~/.claude/settings.json, MCP-Server aus .mcp.json, Skills aus ~/.claude/skills/, die CLAUDE.md im Working Directory. Das ist auf Deiner Maschine praktisch. In CI ist es ein Problem, weil die Maschine dort kein ~/.claude hat und der Run sich in jeder Umgebung anders verhaelt.
2. --bare ist der CI-Schalter
Der Trick für reproduzierbare Runs heisst --bare. Damit uberspringt Claude die Auto-Discovery: keine Hooks, keine Skills, keine Plugins, keine MCP-Server, keine Auto-Memory, keine CLAUDE.md. Er nimmt nur was Du explizit per Flag mitgibst.
claude --bare -p "Fasse diese Datei zusammen" --allowedTools "Read"
Bare-Mode hat ausserdem einen schnelleren Start, weil die ganze Discovery-Phase wegfaellt. Authentifizierung läuft im Bare-Mode ueber ANTHROPIC_API_KEY als Env-Var oder ueber apiKeyHelper im JSON das Du an --settings ubergibst. Kein OAuth, kein Keychain, nichts was nur lokal funktioniert.
Merksatz für den Rest dieses Playbooks: lokale One-Off-Skripte ohne --bare, alles was in CI läuft mit --bare.
3. Strukturierter Output mit --output-format
In CI willst Du normalerweise nicht "ein bisschen Prosa". Du willst was zum Weiterverarbeiten. Drei Output-Formate gibt es:
text(default): die Antwort als Klartextjson: ein JSON-Objekt mitresult,session_id, Token-Usage und Metadatastream-json: newline-delimited JSON, ein Event pro Zeile, für Live-Streaming
Für CI ist json meistens richtig. Du bekommst die Antwort plus alle Metadaten in einem strukturierten Block, kannst per jq filtern, kannst die session_id weiterverwenden.
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"]}'
Mit --json-schema zwingst Du die Antwort in eine Form. Das Ergebnis steckt dann im structured_output-Feld der Response. Wer schonmal LLM-Output mit Regex parsen musste weiss warum das wertvoll ist.
4. Permissions im Headless-Modus
Wenn claude -p einen Prompt bekommt der ein Tool braucht, wuerde er normalerweise fragen. In CI gibt es niemanden der antwortet, der Run wuerde haengen. Zwei Wege das zu loesen.
Erstens, Tool-Whitelist. --allowedTools "Read,Edit,Bash" erlaubt nur diese drei Tool-Kategorien. Du kannst feiner werden mit Pattern-Matching: Bash(git diff *) erlaubt jeden Bash-Befehl der mit git diff anfaengt. Achte auf das Leerzeichen vor dem Stern. Bash(git diff*) ohne Leerzeichen wuerde auch git diff-index matchen, was vermutlich nicht gemeint ist.
Zweitens, Permission-Modes. --permission-mode acceptEdits lässt Claude Files ohne Nachfrage schreiben und akzeptiert ausserdem haeufige FS-Befehle wie mkdir, touch, mv, cp automatisch. --permission-mode dontAsk dagegen blockt alles was nicht in einer Allow-Rule oder dem read-only Set steht, und bricht ab wenn Claude versucht etwas anderes zu tun. Für locked-down CI ist dontAsk mit expliziten Allow-Regeln das sicherere Setup.
5. Der erste echte Skript-Lauf
Bauen wir was Konkretes. Ein Skript das geaenderte Files in Deinem Repo per git diff rauszieht und Claude bittet sie auf Tippfehler zu prüfen. Lokal, kein CI, einfach ein Bash-Script.
#!/bin/bash
# review-staged.sh
set -euo pipefail
CHANGED=$(git diff --name-only --cached)
if [ -z "$CHANGED" ]; then
echo "Keine staged Aenderungen."
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
Mach das Skript executable, leg es als Pre-Commit-Hook ab, fertig. Das erste echte Setup wo Du claude -p täglich benutzt ohne dass es sich wie "AI" anfuehlt.
6. Auf nach GitHub Actions
Jetzt das Ganze in CI. Ein Workflow der bei jedem PR die geaenderten Files reviewt und das Ergebnis als Kommentar an den PR hängt.
Im Repo unter .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 ist hier wichtig. Ohne wuerde Claude versuchen ~/.claude zu laden, das auf dem GitHub-Runner nicht existiert. Mit --bare bekommst Du auf jeder Maschine das gleiche Verhalten.
7. Streaming Output und Retries beobachten
Wenn der Run länger dauert willst Du sehen was passiert. --output-format stream-json zusammen mit --verbose --include-partial-messages gibt Dir ein Event pro Zeile. Filterbar mit 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'
Was Du im Stream noch siehst sind System-Events. Bei API-Fehlern sendet Claude system/api_retry mit attempt, max_retries, retry_delay_ms und einer error-Kategorie wie rate_limit oder server_error. Das ist Gold für CI: Du kannst das in Logs einbauen, kannst eigenen Backoff implementieren, kannst bei rate_limit den Run anders pausieren als bei authentication_failed.
Dazu kommt system/init, das First-Event mit Session-Metadata. Da steht drin welches Modell läuft, welche Tools verfuegbar sind, welche MCP-Server geladen wurden, welche Plugins. Wenn Du in CI sicherstellen willst dass ein bestimmtes Plugin geladen ist, parse das plugins-Array oder das plugin_errors-Array und lass den Run failen wenn was fehlt.
8. Context-Injection für CI-Runs
Bare-Mode laedt nichts. Das ist sauber, aber manchmal brauchst Du Kontext. Vier Flags helfen:
--append-system-prompt "Sei knapp"hängt eine Anweisung an den System-Prompt--append-system-prompt-file path/to/style.mdmacht das gleiche aus einer Datei--settings settings.jsonlaedt eine Settings-Datei mit Tools, Permissions, API-Helper--mcp-config mcp.jsonlaedt MCP-Server explizit--agents '{"reviewer": {...}}'definiert Sub-Agents--plugin-Dir ./my-pluginlaedt einen lokalen Plugin-Ordner
In CI versionierst Du das alles im Repo. Eine .claude-ci/style-guide.md für Tonalitaet, eine .claude-ci/mcp.json für die Server die der Run braucht. Beim Run zeigst Du mit Flags drauf, alles deterministisch, alles im Git.
9. Cost und Budget unter Kontrolle
In CI laufen Runs ohne dass jemand zuschaut. Das heisst auch: ein bug oder ein endloser Loop kann teuer werden. Drei Schutzmassnahmen die ich immer einbaue.
Erstens, kleine Modelle wo es geht. Für Lint-Reviews und kurze Diff-Checks reicht Haiku locker, der ist Faktor 10 billiger als Opus. Per --model claude-haiku-4-5 setzt Du das pro Run. Für den PR-Review oben spart das in einem mittelgrossen Repo schnell 5 Euro pro Tag.
Zweitens, harte Tool-Whitelist. Je weniger Tools erlaubt sind, desto kleiner die Loop-Gefahr. Wenn Claude in CI nur Read braucht, gib ihm nur Read. Kein Bash, kein Edit, kein Web-Fetch.
Drittens, Timeout im CI-Job. GitHub Actions hat timeout-minutes auf Job-Ebene. Setz das auf was Realistisches plus 50% Puffer. Wenn der Run normal 4 Minuten dauert, setz timeout-minutes: 8. Bei einem Loop knallt der Job rechtzeitig statt Stunden zu fressen.
10. Was als naechstes
Wenn Headless läuft, öffnen sich drei Pfade. Erstens, parallel zum CI-Review ein lokaler Cron-Job der jede Nacht ein Audit macht und das Ergebnis als Issue ins Repo schreibt. Zweitens, Multi-Agent: ein Run ruft claude -p mit verschiedenen --agents-Definitionen, jeder Agent prüft einen Aspekt, am Ende ein Composite-Report. Drittens, Python- oder TypeScript-SDK statt CLI, falls Du komplexere Ablaeufe willst mit Tool-Approval-Callbacks und nativen Message-Objekten.
Für den ersten Pfad lies das Recipe 5.2-schedule-routines. Für den zweiten 12.2-agent-research-orchestrate. Für den dritten direkt die Agent-SDK-Doku, da geht es ueber das was per CLI erreichbar ist hinaus.
Source
Spezifika in diesem Playbook (Flags, Output-Formate, Events) verifiziert gegen die offizielle Anthropic-Doku:
- Headless-Mode +
--bare+--output-format+ Stream-Events: https://code.claude.com/docs/en/headless - Die Beispiele zur PR-Review-Pipeline und zur Cost-Begrenzung sind eigene Praxis, keine Doku-Zitate.