Claude Code en CI: headless con claude -p, --bare y GitHub Actions — Cesar Ayala
← Todos los artículos

Claude Code en CI: headless con claude -p, --bare y GitHub Actions

Agrega -p (o --print) a cualquier comando claude y Claude Code corre sin interactividad: lee stdin e imprime un resultado. Todas las banderas del CLI siguen funcionando. Combínalo con --allowedTools para auto-aprobar herramientas, --output-format json para costo y datos de sesión, y --bare para un CI reproducible. Esa bandera vuelve a Claude Code una herramienta scriptable y lista para CI.

claude -p: corre Claude Code sin interactividad (y todas las banderas siguen sirviendo)

Agrega -p (o --print) a cualquier comando claude y Claude Code deja de ser un REPL: lee stdin, ejecuta el prompt y escribe un resultado a stdout, sin pedirte nada. Todas las banderas del CLI siguen funcionando igual que en modo interactivo, así que puedes combinar -p con --allowedTools, --output-format, --bare y lo que necesites. Esa sola bandera convierte a Claude Code en una herramienta scripteable y lista para CI.

El caso mínimo es este:

claude -p "resume los cambios de este repo en una línea"

Corre, imprime la respuesta y termina. Para usarlo así necesitas una cuenta Pro, Max, Team o Enterprise, o un proveedor como Amazon Bedrock, Google Vertex o Azure Foundry. Si vienes del modo interactivo, la única diferencia real es que no hay pantalla: entra un prompt, sale un texto. Si todavía no conoces la herramienta base, empieza por qué es Claude Code y cómo usarlo y luego vuelve aquí para automatizarla.

Lo demás de este post es apilar banderas encima de -p hasta tener un comando reproducible que puedas pegar en un pipeline.

Pasas un promptComo argumento o por stdin
Corre headlessSin TUI: lee, piensa, usa las tools permitidas
Imprime un resultadotexto por defecto, o JSON con metadata
Exit code + stdoutTu script o job de CI lee ambos
claude -p es el mismo CLI sin el loop interactivo.

Auto-aprueba herramientas con –allowedTools y la sintaxis de reglas de permiso

En una terminal interactiva Claude Code te pregunta antes de correr una herramienta. En CI no hay nadie para responder, así que tienes que declarar de antemano qué permites con --allowedTools:

claude -p "corre los tests y arregla el que falle" \
  --allowedTools "Read,Edit,Bash"

Eso auto-aprueba las herramientas listadas sin prompt. La forma escueta (Read, Edit, Bash) abre la herramienta completa, que casi nunca es lo que quieres en un pipeline. La sintaxis de reglas de permiso te deja acotar por comando con coincidencia de prefijo:

claude -p "revisa el diff contra main" \
  --allowedTools "Read,Bash(git diff *),Bash(git log *)"

Aquí el espacio importa muchísimo. Bash(git diff *) coincide con git diff seguido de un espacio y luego cualquier cosa; Bash(git diff*) es una regla distinta que además caza git difftool. No los confundas: en CI esa diferencia es la que separa una regla de mínimo privilegio de un agujero. Trata cada regla como una allowlist explícita y no pongas Bash a secas salvo que de verdad quieras darle la terminal entera.

-p / --printcorre sin interactividad, lee stdin
--allowedToolsauto-aprueba con reglas de permiso
--output-format jsonresult, session_id, total_cost_usd
--bareCI reproducible, auth por API key

–output-format text, json y stream-json: toma result, session_id y total_cost_usd

Por defecto -p imprime texto plano, perfecto para leer en la terminal pero inútil para un script que necesita datos. La bandera --output-format cambia eso y acepta tres valores: text, json y stream-json.

claude -p "explica este error" --output-format json

Con json recibes un objeto estructurado con el texto en result, un session_id para reanudar después, metadata de la corrida y, lo más útil para CI, total_cost_usd con un desglose de costo por modelo. Eso te deja poner un tope de gasto o loguear cuánto costó cada corrida del pipeline. Para extraer un campo, canaliza a jq:

cost=$(claude -p "audita este módulo" \
  --output-format json | jq -r '.total_cost_usd')
echo "Esta corrida costó: $cost USD"

El tercer formato es stream-json: eventos JSON delimitados por saltos de línea que van llegando en vivo. Úsalo con --verbose --include-partial-messages y parséalo con jq cuando quieras seguir la ejecución en tiempo real en vez de esperar al final. Para un job de CI corto casi siempre te conviene json; stream-json brilla cuando la tarea es larga y quieres ver progreso en los logs.

–json-schema: fuerza salida conforme a un esquema en el campo structured_output

Cuando el paso siguiente del pipeline consume la salida, no basta con texto libre: necesitas una forma garantizada. Pasa --json-schema junto con --output-format json y Claude Code devuelve una salida conforme al esquema en el campo structured_output:

claude -p "clasifica la severidad de este issue" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"severity":{"type":"string","enum":["low","medium","high"]},"summary":{"type":"string"}},"required":["severity","summary"]}'

La respuesta trae el objeto validado en structured_output, así que tu triage de issues puede leer .structured_output.severity sin adivinar con regex ni rezarle al parser. Esto es lo que vuelve confiable un pipeline: en lugar de un párrafo que a veces trae el dato y a veces no, obtienes un contrato. Si vienes del mundo de la API directa, es la misma idea de salida estructurada que verías en cómo usar la API de Claude, pero aquí Claude Code se encarga del andamiaje.

–bare: omite hooks, skills, plugins, MCP y CLAUDE.md para un CI reproducible

El mayor riesgo de correr Claude Code en CI es que el resultado dependa de qué hay instalado en la máquina. Un hook local, un plugin, un servidor MCP o un CLAUDE.md distinto pueden cambiar la salida sin que lo notes. La bandera --bare mata esa varianza:

ANTHROPIC_API_KEY="$CLAUDE_KEY" claude -p "revisa el diff" \
  --bare \
  --allowedTools "Read,Bash(git diff *)"

En modo --bare, Claude Code se salta el auto-descubrimiento de hooks, skills, plugins, servidores MCP, la memoria automática y CLAUDE.md. Solo aplican las banderas que pasas explícitamente, así que obtienes el mismo resultado en tu laptop, en el runner de CI y en el de tu compañero. Es el modo recomendado para llamadas scripteadas y desde el SDK, y va a volverse el default de -p en una versión futura, así que vale la pena adoptarlo ya.

Ojo con la autenticación: en modo bare no se usa tu login interactivo, la auth tiene que venir de ANTHROPIC_API_KEY (o de las credenciales del proveedor). Eso encaja perfecto con CI, donde de todas formas guardas la llave como secreto.

  1. Agrega -pcorre sin interactividad, lee stdin, imprime result
  2. Acota con --allowedToolsreglas de minimo privilegio como Bash(git diff *)
  3. Pide --output-format jsonresult, session_id y total_cost_usd para el pipeline
  4. Blinda con --baresin hooks/skills/MCP/CLAUDE.md; auth por ANTHROPIC_API_KEY

Encauza datos por Claude y agrega un script lint:claude a package.json

Como -p lee stdin, cualquier salida de otro comando puede entrar directo. El patrón clásico es explicar un error de build:

cat build-error.txt | claude -p 'explica la causa raíz de este error de build' > output.txt

Sale el texto, entra el archivo, y el resultado se guarda donde tú quieras. Un detalle operativo: el stdin canalizado tiene un tope de 10 MB. Si tu entrada es más grande, escríbela a un archivo y pídele a Claude que lo lea con la herramienta Read en lugar de embutirlo por el pipe.

Este patrón se presta muy bien para un linter que vive en tu package.json:

{
  "scripts": {
    "lint:claude": "git diff main | claude -p \"you are a typo linter, report only real typos\""
  }
}

Ahora npm run lint:claude canaliza el diff contra main y te reporta erratas. Es un ejemplo pequeño, pero muestra el modelo mental completo: Claude Code se comporta como cualquier otra herramienta de línea de comandos que encadenas con pipes y redirecciones.

–continue y –resume: captura el session_id y mantén viva la conversación

A veces una corrida sola no basta y quieres seguir la misma conversación en un paso posterior del pipeline. Tienes dos banderas: --continue retoma la conversación más reciente, y --resume <session_id> retoma una específica por id. Para lo segundo, captura el session_id de la salida JSON:

session_id=$(claude -p "analiza los cambios de este PR" \
  --output-format json | jq -r '.session_id')

claude -p "ahora sugiere tests para esos cambios" \
  --resume "$session_id" \
  --output-format json

El primer comando guarda el id; el segundo reanuda exactamente esa sesión con todo su contexto. En un pipeline esto te deja partir un trabajo largo en etapas —analizar, luego proponer, luego escribir— sin perder el hilo entre pasos. Si estás armando algo más ambicioso encima de esto, el patrón de estado por sesión es el mismo que usarías al crear un agente autónomo con Sonnet 5.

–append-system-prompt: convierte una llamada suelta en un revisor de seguridad

A veces no quieres cambiar el comportamiento base de Claude Code, solo sumarle una instrucción o una persona. Para eso está --append-system-prompt, que agrega texto al system prompt sin borrar el default:

git diff main | claude -p "revisa este diff" \
  --append-system-prompt "Eres un revisor de seguridad senior. Enfocate en inyeccion, manejo de secretos y validacion de entrada. Se conciso." \
  --allowedTools "Read" \
  --output-format json

Con eso, la misma llamada genérica se vuelve un revisor de seguridad especializado, sin escribir un agente aparte ni tocar tu configuración global. Es la palanca ideal para tener varios roles —revisor de seguridad, linter de estilo, triager de issues— compartiendo el mismo binario y cambiando solo esta bandera. Si de todas formas expones servidores MCP a estos agentes, revisa cómo asegurar servidores MCP antes de darles acceso a datos reales.

Modos de permiso: acceptEdits vs dontAsk para un CI blindado

--allowedTools dice qué se permite; --permission-mode dice qué pasa con todo lo demás. Hay dos modos que importan para automatización. Con acceptEdits, Claude Code auto-aprueba las ediciones de archivos y comandos comunes de sistema de archivos como mkdir, touch, mv y cp, lo cual es cómodo cuando quieres que el agente escriba libremente:

claude -p "aplica el fix y crea los archivos que falten" \
  --permission-mode acceptEdits \
  --allowedTools "Read,Edit,Bash(git diff *)"

Para CI de verdad blindado usa dontAsk, que niega cualquier cosa que no esté en tus reglas de allow o en el set de solo lectura. Nada se cuela por default:

claude -p "revisa el diff y comenta hallazgos" \
  --permission-mode dontAsk \
  --allowedTools "Read,Bash(git diff *)" \
  --max-turns 6 \
  --max-budget-usd 0.50

Fíjate en las dos redes de seguridad extra: --max-turns topa cuántas idas y vueltas hace el agente, y --max-budget-usd corta la corrida si el costo pasa el límite. En un pipeline que corre en cada PR, esos dos topes son lo que evita que un prompt mal escrito te queme presupuesto o gire en un loop. Si además quieres afinar costo contra calidad eligiendo modelo y esfuerzo, eso lo cubro en ajustar Sonnet 5: costo, calidad, effort y batch.

acceptEdits

  • Auto-aprueba ediciones de archivos
  • Permite mkdir, touch, mv, cp
  • Bueno para agentes que escriben
  • Menos blindado, mas permisivo

dontAsk

  • Niega todo lo que no este en allow
  • Solo tus reglas mas el set de lectura
  • Ideal para revision de PRs
  • CI blindado de minimo privilegio

Conéctalo a GitHub Actions y GitLab CI: key como secreto y mínimo privilegio

Con todo lo anterior armado, meterlo en CI es directo: Claude Code tiene soporte oficial para GitHub Actions y GitLab CI/CD, pensado para revisión de PRs, triage de issues y code review en cada push. La regla de oro es guardar la API key como secreto del CI (nunca en el repo) y correr con --bare más --allowedTools de mínimo privilegio.

Un paso mínimo en GitHub Actions para revisar cada PR se ve así:

name: Revision con Claude Code
on: pull_request
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Instalar Claude Code
        run: npm install -g @anthropic-ai/claude-code
      - name: Revisar el diff del PR
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          git diff origin/${{ github.base_ref }}...HEAD \
            | claude -p "revisa este diff: bugs, seguridad y estilo" \
                --bare \
                --permission-mode dontAsk \
                --allowedTools "Read" \
                --max-budget-usd 0.50 \
                --output-format json > review.json
          jq -r '.result' review.json

En GitLab CI/CD el patrón es idéntico: defines ANTHROPIC_API_KEY como variable protegida y enmascarada del proyecto, y corres el mismo comando claude -p --bare dentro de un job. En ambos casos, --bare garantiza que el runner produzca el mismo resultado sin importar qué tenga instalado, y --allowedTools "Read" con dontAsk deja al agente leer el diff pero no tocar nada más. Para triage de issues, cambia el prompt y agrega --json-schema para que la salida en structured_output alimente directo tu etiquetado.

Ese es todo el recorrido: -p te da headless, --allowedTools y los modos de permiso te dan control, --output-format json te da datos, y --bare te da reproducibilidad. Junta las cuatro y Claude Code deja de ser un asistente interactivo para volverse una pieza más de tu pipeline. Desde el hub de agentes de IA puedes seguir con los subagentes, hooks y slash commands para orquestar corridas más grandes.

Fuentes oficiales: Claude Code headless, referencia del CLI y GitHub Actions.