← Alle Playbooks
Playbook· build

Claude Code headless en CI/CD, un setup que ya corre mañana

claude -p es el modo workhorse para scripting. Construimos un workflow de GitHub Actions que revisa tests en cada PR y auto-corrige errores de lint, sin que nadie tenga que mirar.

La mayoría usa Claude Code de forma interactiva. Tú escribes, él responde, los dos hacéis un pequeño baile. Funciona. Pero en algún momento quieres ejecutar el mismo modelo de forma automatizada, en un cron job, en GitHub Actions, como un pre-commit hook que comprueba algo. Justo para eso está el modo -p, a veces llamado --print. Una sola petición, una respuesta, exit. Scriptable, idempotente, listo para CI.

Si llevas todo esto hasta el final, después de 30 minutos tienes un workflow de GitHub Actions que en cada pull request ejecuta un code review y escribe el resultado de vuelta como comentario. Más dos scripts para cron jobs locales. Más la comprensión de por qué --bare es la diferencia entre "en mi máquina" y "en cualquier máquina".

1. Qué es claude -p en realidad

claude -p "Deine frage" arranca Claude Code, envía exactamente este prompt, imprime la respuesta a stdout, sale. Sin TUI, sin live stream, sin "Hola, ¿qué puedo hacer?". Un comando de shell como cualquier otro.

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

Puedes colgarle todo lo que la versión interactiva también entiende. --allowedTools "Read,Edit,Bash" para que arranque sin un prompt de permiso. --output-format json si quieres redirigir el resultado por pipe. --continue si la siguiente petición debe continuar en la misma sesión. Eso es justo lo que hace de claude -p el workhorse para todo lo automatizado.

Una cosa que muchos pasan por alto: sin más flags claude -p carga el contexto completo que también cargaría una sesión interactiva. Hooks de ~/.claude/settings.json, servidores MCP de .mcp.json, skills de ~/.claude/skills/, el CLAUDE.md en el working directory. Eso es práctico en tu máquina. En CI es un problema, porque la máquina ahí no tiene ~/.claude y la ejecución se comporta distinto en cada entorno.

2. --bare es el interruptor de CI

El truco para ejecuciones reproducibles es --bare. Con él, Claude se salta el auto-discovery: sin hooks, sin skills, sin plugins, sin servidores MCP, sin auto-memory, sin CLAUDE.md. Coge solo lo que le pasas explícitamente vía flag.

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

El bare mode tiene además un arranque más rápido, porque toda la fase de discovery desaparece. La autenticación en bare mode corre vía ANTHROPIC_API_KEY como env var o vía apiKeyHelper en el JSON que le pasas a --settings. Sin OAuth, sin keychain, nada que solo funcione en local.

Regla mnemotécnica para el resto de este playbook: scripts locales de una sola vez sin --bare, todo lo que corre en CI con --bare.

3. Output estructurado con --output-format

En CI normalmente no quieres "un poco de prosa". Quieres algo para procesar después. Hay tres formatos de output:

  • text (default): la respuesta como texto plano
  • json: un objeto JSON con result, session_id, uso de tokens y metadata
  • stream-json: JSON delimitado por saltos de línea, un evento por línea, para live streaming

Para CI json suele ser lo correcto. Recibes la respuesta más toda la metadata en un bloque estructurado, puedes filtrar vía jq, puedes reutilizar el 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"]}'

Con --json-schema fuerzas la respuesta a una forma. El resultado se mete entonces en el campo structured_output de la respuesta. Cualquiera que alguna vez haya tenido que parsear output de LLM con regex sabe por qué eso es valioso.

4. Permisos en modo headless

Cuando claude -p recibe un prompt que necesita un tool, normalmente preguntaría. En CI no hay nadie que responda, la ejecución se quedaría colgada. Dos formas de resolver esto.

Primero, whitelist de tools. --allowedTools "Read,Edit,Bash" permite solo estas tres categorías de tools. Puedes afinar más con pattern matching: Bash(git diff *) permite cualquier comando Bash que empiece por git diff . Fíjate en el espacio antes del asterisco. Bash(git diff*) sin espacio también haría match con git diff-index, lo que probablemente no es lo que se quiere.

Segundo, permission modes. --permission-mode acceptEdits deja que Claude escriba archivos sin preguntar y además acepta automáticamente comandos FS habituales como mkdir, touch, mv, cp. --permission-mode dontAsk en cambio bloquea todo lo que no esté en una allow rule o en el set de solo lectura, y aborta si Claude intenta hacer otra cosa. Para un CI blindado, dontAsk con allow rules explícitas es el setup más seguro.

5. La primera ejecución real de script

Construyamos algo concreto. Un script que saca los archivos modificados en tu repo vía git diff y le pide a Claude que los compruebe en busca de errores tipográficos. En local, sin CI, simplemente un script Bash.

#!/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

Haz el script ejecutable, déjalo como pre-commit hook, listo. El primer setup real donde usas claude -p a diario sin que se sienta como "AI".

6. A por GitHub Actions

Ahora todo esto en CI. Un workflow que en cada PR revisa los archivos modificados y adjunta el resultado como comentario al PR.

En el repo bajo .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 es importante aquí. Sin él Claude intentaría cargar ~/.claude, que no existe en el runner de GitHub. Con --bare obtienes el mismo comportamiento en cada máquina.

7. Observa el output en streaming y los retries

Cuando la ejecución dura más quieres ver qué pasa. --output-format stream-json junto con --verbose --include-partial-messages te da un evento por línea. Filtrable con 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'

Lo que también ves en el stream son eventos de sistema. En errores de API Claude envía system/api_retry con attempt, max_retries, retry_delay_ms y una categoría error como rate_limit o server_error. Eso es oro para CI: puedes integrarlo en los logs, puedes implementar tu propio backoff, puedes pausar la ejecución de forma distinta ante rate_limit que ante authentication_failed.

A eso se suma system/init, el primer evento con metadata de sesión. Ahí está qué modelo corre, qué tools están disponibles, qué servidores MCP se cargaron, qué plugins. Si quieres asegurarte en CI de que un plugin concreto está cargado, parsea el array plugins o el array plugin_errors y deja que la ejecución falle si falta algo.

8. Inyección de contexto para ejecuciones de CI

El bare mode no carga nada. Eso es limpio, pero a veces necesitas contexto. Cuatro flags ayudan:

  • --append-system-prompt "Sei knapp" cuelga una instrucción al system prompt
  • --append-system-prompt-file path/to/style.md hace lo mismo desde un archivo
  • --settings settings.json carga un archivo de settings con tools, permisos, API helper
  • --mcp-config mcp.json carga servidores MCP explícitamente
  • --agents '{"reviewer": {...}}' define sub-agentes
  • --plugin-Dir ./my-plugin carga una carpeta de plugin local

En CI versionas todo esto en el repo. Un .claude-ci/style-guide.md para el tono, un .claude-ci/mcp.json para los servidores que la ejecución necesita. En la ejecución apuntas a ello con flags, todo determinista, todo en Git.

9. Coste y presupuesto bajo control

En CI las ejecuciones pasan sin que nadie mire. Eso también significa: un bug o un loop infinito puede salir caro. Tres medidas de protección que siempre integro.

Primero, modelos pequeños donde se pueda. Para reviews de lint y comprobaciones cortas de diff Haiku sobra de largo, es un factor 10 más barato que Opus. Con --model claude-haiku-4-5 lo fijas por ejecución. Para el PR review de arriba eso ahorra rápido 5 euros al día en un repo de tamaño medio.

Segundo, whitelist dura de tools. Cuantos menos tools permitidos, menor el riesgo de loop. Si Claude en CI solo necesita Read, dale solo Read. Sin Bash, sin Edit, sin web fetch.

Tercero, timeout en el job de CI. GitHub Actions tiene timeout-minutes a nivel de job. Ponlo en algo realista más un 50% de margen. Si la ejecución normalmente tarda 4 minutos, pon timeout-minutes: 8. Ante un loop el job salta a tiempo en vez de comerse horas.

10. Qué viene después

Una vez que headless funciona, se abren tres caminos. Primero, en paralelo al review de CI, un cron job local que cada noche hace una auditoría y escribe el resultado como issue en el repo. Segundo, multi-agente: una ejecución llama a claude -p con distintas definiciones --agents, cada agente comprueba un aspecto, al final un informe compuesto. Tercero, el SDK de Python o TypeScript en vez de la CLI, por si quieres flujos más complejos con callbacks de tool-approval y objetos de mensaje nativos.

Para el primer camino lee la recipe 5.2-schedule-routines. Para el segundo 12.2-agent-research-orchestrate. Para el tercero, directamente la documentación del Agent SDK, donde va más allá de lo que es alcanzable vía CLI.

Source

Detalles concretos en este playbook (flags, formatos de output, eventos) verificados contra la documentación oficial de Anthropic:

  • Modo headless + --bare + --output-format + eventos de stream: https://code.claude.com/docs/en/headless
  • Los ejemplos del pipeline de PR review y de la limitación de coste son práctica propia, no citas de la documentación.