Receta · Intermedio · 15 min
Claude Code headless: claude -p en scripts y cron paso a paso
Usa claude -p en un script de cron: resumen diario de commits con permisos mínimos, tope de turnos, salida JSON y login de la suscripción. Probado.
Probado con Claude Code 2.1.316 · revisado
Ver los archivos en GitHub Gratis · licencia MIT
El problema
Hay tareas que le pides a Claude Code todos los días con las mismas palabras: resumir los commits de ayer, explicar un log de error, revisar un diff antes de subirlo. Claude Code no necesita su interfaz para hacerlas: con claude -p recibe un prompt, hace la tarea y devuelve texto o JSON, así que se puede llamar desde un script o desde cron. Lo difícil son los detalles que nadie cuenta: qué pasa con los permisos cuando no hay nadie delante, cómo funciona el login en cron y por qué el modo sin interfaz puede gastar bastante más de tu límite de 5 horas.
La solución
Vamos a montar un resumen diario de commits que se ejecuta solo, con permisos mínimos, tope de turnos y salida JSON comprobable. Todos los archivos están en el repositorio gratuito ctrlia-claude-code-headless-cron.
1. Prueba claude -p a mano
La bandera -p (o --print) ejecuta Claude Code de forma no interactiva: lee el prompt (y lo que le pases por la entrada estándar), trabaja con sus herramientas y escribe el resultado en la salida estándar. Sale con código 0 si todo va bien y distinto de 0 si falla, así que tu script puede decidir qué hacer.
claude -p "¿Qué hace el módulo auth?"
Si responde, ya tienes todo lo necesario. Úsalo para tareas que se repiten y tienen un resultado claro: resumir los commits del día, explicar un log de error, revisar un diff antes de subirlo o generar notas de versión. No lo uses para tareas abiertas como «mejora el proyecto»: sin nadie que revise, una tarea vaga gasta mucho y produce poco. Para los pull requests de GitHub ya tenemos la receta de revisión de PR con GitHub Actions; esta es para tu PC o tu servidor, sin GitHub de por medio.
2. Da solo los permisos que necesita
En una sesión normal Claude te pregunta antes de editar o ejecutar algo. En -p no hay nadie a quien preguntar. Desde la v2.1.302, si nada fija un modo de permisos, claude -p arranca en modo Manual: lo que necesite aprobación se deniega. Lo comprobamos: al pedirle «crea hola.txt» sin permisos, respondió que no podía y el JSON trajo la llamada en permission_denials:
{
"is_error": false,
"result": "No he podido crear `hola.txt`. El permiso para escribir ... fue denegado automáticamente, porque esta sesión no tiene forma de pedir aprobación.",
"permission_denials": [
{"tool_name": "Write", "tool_input": {"file_path": ".../hola.txt", "content": "hola\n"}}
]
}
Tienes tres palancas:
--allowedTools "Bash(git log *)"aprueba solo lo que nombras. El espacio antes de*importa:Bash(git diff*)también aceptaríagit diff-index.--permission-mode acceptEditsdeja escribir archivos sin preguntar, pero no ejecutar otros comandos.--permission-prompts none(v2.1.259 o posterior) le dice a Claude que nadie puede aprobar y que no reintente: ideal para cron.
Ojo con los comandos compuestos: con Bash(python3 -m unittest *) aprobado, Claude intentó python3 -m unittest ... ; echo $? y se le denegó, porque la regla no cubre la parte de echo. Da el mínimo, y si algo falta lo verás en permission_denials. Las reglas se explican a fondo en la receta de permisos de Claude Code.
3. Pide salida JSON (y con esquema si la necesitas)
Con --output-format json recibes un objeto con result (el texto), is_error, num_turns, session_id, permission_denials y total_cost_usd (una estimación a precio de lista, no tu factura). Si necesitas datos con forma fija, añade --json-schema con un JSON Schema: la respuesta trae structured_output con esos campos.
claude -p "Resume en español los commits de las últimas 24 horas (git log --since=24.hours --oneline)." \
--model haiku --max-turns 4 \
--permission-prompts none \
--allowedTools "Bash(git log *)" \
--no-session-persistence \
--output-format json \
--json-schema '{"type":"object","properties":{"resumen":{"type":"string"},"commits":{"type":"integer"}},"required":["resumen","commits"]}' \
< /dev/null | jq '.structured_output'
En nuestra prueba, con el esquema {resumen: string, commits: integer}, devolvió {"resumen": "Hay 2 commits...", "commits": 2}. La documentación avisa de que si Claude no llega a producir la salida (por ejemplo, porque rechaza la tarea), el campo no aparece: compruébalo antes de usarlo.
Para leer el JSON en Linux, instala jq (sudo apt install jq) y usa jq -r '.result' o jq -e '.is_error == false', que devuelve error si la ejecución falló. Si quieres ver el progreso en tiempo real, --output-format stream-json --verbose emite un evento JSON por línea; para cron no hace falta.
4. Pon límites: turnos, presupuesto, modelo y entrada estándar
Pon siempre un techo:
--max-turns 6corta la ejecución con error si Claude se enrolla.--max-budget-usd 0.50para cuando el gasto estimado llega a esa cifra (puede pasarse un poco, así que deja margen).--model haikubasta para resumir. En nuestra prueba, el mismo encargo costó 0,009 USD estimados con Haiku frente a 0,20 USD con Opus, porque Opus además cargó todo el contexto de la configuración.--no-session-persistenceevita guardar en disco una sesión que nunca vas a reanudar.< /dev/nullal final: sin él, Claude Code espera 3 segundos por si le llega algo por la entrada estándar y escribe en stderrWarning: no stdin data received in 3s.
Si llamas a Claude con una clave de API (no con tu suscripción), añade --bare: no carga hooks, skills, plugins, servidores MCP ni CLAUDE.md, así que el resultado es igual en cualquier máquina. Con la suscripción no sirve, porque --bare no lee el login guardado.
5. Crea el script de cron
Guarda este script en ~/bin/resumen-git.sh y dale permiso de ejecución con chmod +x ~/bin/resumen-git.sh. Cambia PROYECTO por la ruta de tu repositorio (o pásala como variable de entorno):
#!/usr/bin/env bash
# Resumen diario de commits con Claude Code en modo sin interfaz
set -euo pipefail
export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
# Servidor sin navegador: descomenta si usas un token de claude setup-token
# source "$HOME/.config/claude-token.env"
command -v jq > /dev/null || { echo "Falta jq: instálalo con sudo apt install jq" >&2; exit 1; }
PROYECTO="${PROYECTO:-$HOME/proyectos/mi-app}"
DESTINO="${DESTINO:-$HOME/informes}"
mkdir -p "$DESTINO"
cd "$PROYECTO"
salida="$DESTINO/git-$(date +%F).json"
claude -p "Resume en español los commits de las últimas 24 horas (usa git log --since=24.hours --stat). Agrúpalos por tema y marca con [REVISAR] lo que parezca arriesgado. Si no hay commits, dilo en una línea." \
--model haiku \
--max-turns 6 \
--max-budget-usd 0.50 \
--permission-prompts none \
--allowedTools "Bash(git log *)" \
--no-session-persistence \
--output-format json \
< /dev/null > "$salida"
# Falla (y cron lo anota en el log) si la ejecución terminó en error
jq -e '.is_error == false' "$salida" > /dev/null
jq -r '.result' "$salida" > "${salida%.json}.md"
Tres detalles de cron que rompen casi todos los primeros intentos, y que el script ya resuelve:
- El PATH: cron usa un PATH mínimo y no encuentra
claude, que el instalador deja en~/.local/bin; por eso el script lo exporta. - El directorio: cron empieza en tu carpeta personal, así que el script hace
cdal proyecto. - Ejecuciones solapadas: si un día tarda más,
flock -n(en la línea de crontab del paso siguiente) evita que arranque una segunda copia.
6. Prográmalo en cron y resuelve el login
Abre tu crontab con crontab -e y añade esta línea. Lo lanza de lunes a viernes a las 7:30 y guarda la salida y los errores en un log:
30 7 * * 1-5 flock -n /tmp/resumen-git.lock "$HOME/bin/resumen-git.sh" >> "$HOME/informes/cron.log" 2>&1
Para el login: en Linux, tu sesión de la suscripción se guarda en ~/.claude/.credentials.json, y cron, que corre con tu usuario, la usa. Si el servidor no tiene navegador o la sesión caduca, ejecuta claude setup-token en tu PC: genera un token de un año que pones en la variable CLAUDE_CODE_OAUTH_TOKEN, guardada en un archivo con permisos 600 y nunca en el repositorio:
# En tu PC (abre el navegador una vez y muestra el token; no lo guarda)
claude setup-token
# En el servidor: guárdalo fuera del repositorio y solo legible por ti
install -m 600 /dev/null ~/.config/claude-token.env
echo 'export CLAUDE_CODE_OAUTH_TOKEN=pega-aqui-el-token' > ~/.config/claude-token.env
# Y al principio de resumen-git.sh (la línea ya está, comentada):
# source "$HOME/.config/claude-token.env"
7. Vigila el consumo: tu límite puede durar menos
El issue #97074, abierto el 25 de septiembre de 2026 y con 146 reacciones, mide en un plan Max que la misma carga de trabajo con claude -p gasta unas 1,8 veces más de la ventana de 5 horas que en la sesión interactiva, con el mismo número de tokens (prueba A/B con la v2.1.282 y Opus 5). La página de ayuda del Agent SDK dice que claude -p consume de los límites de tu suscripción, pero no que lo haga con otro peso, y Anthropic no lo ha documentado ni corregido a fecha de hoy (1-nov-2026). No es un dato oficial: tómalo como un riesgo.
En la práctica: usa un modelo pequeño para tareas pequeñas, limita turnos, no lances decenas de ejecuciones al día con la suscripción, revisa /usage en una sesión normal la primera semana y, si automatizas mucho, valora una clave de API con --max-budget-usd, donde pagas lo que gastas. Revisaremos el issue cada mes. Si dudas de qué plan te conviene, mira cuánto cuesta Claude Code.
Compruébalo
Ejecuta el script a mano antes de dejarlo en cron:
PROYECTO="$HOME/proyectos/mi-app" ~/bin/resumen-git.sh; echo "exit $?"
cat ~/informes/git-$(date +%F).md
Debe terminar con exit 0 y dejar en ~/informes/ el .json de la ejecución y el .md con el resumen. Si la ejecución falla (is_error a true), el script sale con error y, en cron, el motivo queda en ~/informes/cron.log.
Lo que comprobamos con Claude Code 2.1.316 en Linux el 1-nov-2026: --json-schema devolvió structured_output con 2 commits; con --permission-prompts none y sin permisos, la escritura de hola.txt quedó en permission_denials; un comando compuesto fuera de la regla aprobada se denegó; sin < /dev/null salió el aviso de stdin de 3 segundos; y el coste estimado fue de 0,009 USD con Haiku y 0,20 USD con Opus. El script lo ejecutamos con claude y jq simulados (crea los dos archivos y falla si is_error es true o si falta jq). Aún no lo hemos probado con el jq real ni dejando la línea de cron un día entero, y el consumo extra de #97074 no lo hemos medido, porque habría que gastar límite a propósito.
Por qué funciona
-p convierte Claude Code en un programa de línea de comandos normal: entrada por argumentos y stdin, resultado por stdout y un código de salida que el script puede comprobar. Como no hay nadie para aprobar permisos, Claude Code deniega lo que no esté aprobado de antemano (modo Manual desde la v2.1.302) y lo anota en permission_denials; --allowedTools abre exactamente lo que nombras y --permission-prompts none evita que Claude insista. Los topes (--max-turns, --max-budget-usd) y el modelo pequeño acotan el gasto, y --output-format json da al script un campo is_error fiable para fallar de forma visible. Cron corre con tu usuario, así que usa tu login guardado. Más detalles en la documentación del modo sin interfaz, la referencia de la CLI, la de modos de permisos, la de autenticación y la de costes.
Errores típicos
- El log dice
claude: command not found→ cron usa un PATH mínimo → exportaPATH="$HOME/.local/bin:$PATH"al principio del script o usa la ruta completa que te dacommand -v claude. - Claude responde que no puede escribir o ejecutar algo → desde la v2.1.302
-parranca en modo Manual y deniega lo que necesita aprobación → añade solo lo necesario con--allowedToolso--permission-mode acceptEditsy mirapermission_denialsen el JSON. - Un comando que parecía aprobado se deniega → era un comando compuesto (
...; echo $?) y la regla solo cubre la primera parte → aprueba cada comando o pide en el prompt que no los encadene. - Tarda 3 segundos en arrancar y sale
Warning: no stdin data received in 3s→ Claude Code espera datos por stdin → añade< /dev/nullsi no le pasas nada. --barefalla con la suscripción →--bareno lee el login guardado → úsalo solo con una clave de API.structured_outputno existe en el JSON → Claude no llegó a producir la salida (por ejemplo, rechazó la tarea) → comprueba el campo antes de usarlo.- El servidor pierde el login → no tiene navegador o la sesión caducó → usa
claude setup-tokenyCLAUDE_CODE_OAUTH_TOKENen un archivo con permisos 600.
Preguntas frecuentes
¿Funciona con mi suscripción Pro o Max o necesito una clave de API?
Funciona con la suscripción: claude -p usa el mismo login que la sesión normal y consume de tus límites. La clave de API solo es obligatoria si usas --bare, que no lee el login guardado.
Mi cron no hace nada y el log dice «claude: command not found».
Cron usa un PATH mínimo. Exporta PATH="$HOME/.local/bin:$PATH" al principio del script o usa la ruta completa que te da command -v claude.
¿Por qué tarda 3 segundos en arrancar y sale un aviso de stdin?
Claude Code espera por si le pasas datos por la entrada estándar. Añade < /dev/null al final del comando si no le pasas nada.
Claude dice que no puede escribir un archivo. ¿Está roto?
No: desde la v2.1.302 -p arranca en modo Manual y deniega lo que necesita aprobación. Añade solo lo necesario con --allowedTools o --permission-mode acceptEdits, y mira permission_denials en el JSON.
¿Es verdad que gasta más límite que la sesión normal?
Hay un informe (#97074, abierto, 146 reacciones) que mide unas 1,8 veces más uso de la ventana de 5 h con los mismos tokens. No está documentado ni confirmado por Anthropic. Limita turnos, usa Haiku para tareas pequeñas y vigila /usage.
¿Puedo continuar una ejecución anterior?
Sí, quitando --no-session-persistence: guarda session_id del JSON y lánzalo después con claude -p "sigue" --resume "$session_id", o usa --continue para la última.
¿Y las tareas programadas propias de Claude Code (/loop, rutinas)?
Existen (ver scheduled-tasks y routines en la documentación) y sirven para otros casos; cron con -p es lo más simple cuando ya tienes un servidor Linux y quieres control total.