TL;DR En headless, un permiso denegado no rompe la ejecución.
claude -pdevuelve exit code0,"subtype": "success"y"is_error": falseaunque Claude no haya tocado un solo fichero. La única huella está en el arraypermission_denialsdel JSON, que nadie mira. Léelo y falla el build tú mismo. Y para que Claude sí pueda trabajar,--allowedTools(di exactamente qué puede hacer) o--permission-mode dontAsk(deniega rápido, sin reintentos) antes que el--dangerously-skip-permissionsal que llega todo el mundo.
Tu job de CI llama a claude -p, termina en verde, y el commit no está. No hay error, no hay aviso, no hay nada. Lo miras dos veces, no entiendes nada, y acabas donde acaba medio internet: pegando --dangerously-skip-permissions en el pipeline para que "deje de dar problemas".
El problema es que nunca dio problemas. Dio silencio.
Lo que ves, y lo que pasó de verdad
Lancé esto en un repo limpio, en modo Manual, con un comando que no está en mis reglas allow:
claude -p "Run this exact shell command and nothing else: touch ./needsperm.txt" \
--permission-mode default --output-format json
Lo que devuelve la ejecución:
EXIT CODE : 0
subtype : success
is_error : False
denials : 2 ['Bash', 'Bash']
fichero : NO creado
Exit 0. success. is_error: false. Y el fichero no existe. Claude lo intentó dos veces, el sistema de permisos lo denegó las dos, y la ejecución terminó declarándose un éxito.
En una sesión interactiva esto no te pasa: aparece el diálogo de permisos y pulsas. Con -p no hay nadie a quien preguntar, así que la llamada se deniega y Claude sigue adelante con lo que le queda. Desde fuera, una ejecución que no pudo hacer su trabajo y una que lo hizo entero son indistinguibles.
Cómo detectarlo, que es lo primero
El dato está ahí, solo que fuera del sitio donde miras. Con --output-format json, cada denegación se apunta en permission_denials:
out=$(claude -p "$PROMPT" --output-format json)
denied=$(printf '%s' "$out" | jq '.permission_denials | length')
if [ "$denied" -gt 0 ]; then
echo "Claude fue bloqueado $denied veces:"
printf '%s' "$out" | jq -r '.permission_denials[] |
" \(.tool_name): \(.tool_input.command // .tool_input.file_path // "?")"'
exit 1
fi
Contra la ejecución de arriba, eso imprime:
Claude fue bloqueado 2 veces:
Bash: touch ./needsperm.txt
Bash: touch ./needsperm.txt
Ponlo en el job y tu pipeline deja de mentirte. Esto es lo que vale la pena copiar aunque no cambies nada más.
Y usa printf '%s', no echo. El JSON lleva \n dentro de las cadenas y zsh (el shell por defecto de macOS) los convierte en saltos de línea reales, así que echo "$out" | jq te devuelve un parse error que parece un fallo de Claude Code y es tuyo. En bash funciona, que es justo lo que hace que cueste tanto encontrarlo.
Las cuatro salidas, de mejor a peor
| Opción | Qué hace | Para qué sirve |
|---|---|---|
--allowedTools |
Preaprueba exactamente lo que puede ejecutar | CI donde sabes qué va a hacer Claude |
--permission-mode dontAsk |
Deniega todo lo que pediría permiso, sin esperar | Pipelines cerrados y entornos restringidos |
--permission-mode auto |
Un clasificador aparte revisa cada acción | Tareas largas donde no puedes enumerarlo todo |
--dangerously-skip-permissions |
Se salta las comprobaciones | Contenedores y VMs aisladas, y punto |
1. --allowedTools, la que deberías usar por defecto
Le dices qué puede hacer y lo hace, sin diálogos y sin abrir la puerta a nada más:
claude -p "corrige los errores de lint" \
--allowedTools "Read" "Edit" "Bash(npm run lint*)"
Comprobado en la misma prueba de antes: con la regla puesta, el comando se ejecuta y el fichero aparece.
2. --permission-mode dontAsk, cuando prefieres el portazo
Deniega de golpe todo lo que hubiera pedido permiso, y solo deja pasar tus reglas allow, los comandos de solo lectura y lo que apruebe un hook PreToolUse. La documentación lo describe para pipelines de CI y entornos restringidos, y la clave está en esta frase: "the session never waits for input".
La diferencia práctica con el modo Manual es el ruido. En mi prueba, Manual gastó 3 turnos reintentando el comando denegado; dontAsk lo cortó en 2 y sin reintentos.
3. --permission-mode auto, con un tope que conviene conocer
El clasificador de auto mode revisa cada acción en vez de preguntarte. Funciona bien, pero en headless tiene un final: si bloquea 3 veces seguidas o 20 en total, la sesión se aborta, porque no hay nadie a quien pasarle el turno. Ese es el error que verás:
Agent aborted: too many classifier denials in headless mode
4. --dangerously-skip-permissions, la que casi nunca es la respuesta
Es exactamente lo mismo que --permission-mode bypassPermissions. Y hay un detalle que dice bastante: el clasificador de auto mode bloquea por defecto lanzar un agente autónomo sin aprobación ni sandbox, y nombra esta flag como ejemplo. Claude Code frena cuando Claude Code intenta usarla.
Lo que esa flag no se salta (aunque lo parezca)
Aquí hay más matiz del que sugiere el nombre. Ni en modo bypass se saltan:
- Tus reglas
askexplícitas, que siguen abriendo diálogo - Las herramientas de conector que tu organización puso en
ask - Las herramientas MCP marcadas como
requiresUserInteraction rm -rf /yrm -rf ~, que siguen preguntando como cortacircuitos, incluso metidos dentro de$(...), de comillas invertidas o de<(...)
Y dos negativas que te van a morder si automatizas:
--dangerously-skip-permissions cannot be used with root/sudo privileges
for security reasons
En Linux y macOS se niega a arrancar como root o bajo sudo, salvo dentro de un sandbox reconocido. Y una sesión en background con --bg no arranca en este modo hasta que hayas aceptado el diálogo de responsabilidad una vez en una sesión interactiva. Un contenedor recién creado no lo ha aceptado nunca.
Referencia
| Modo en headless | Qué pasa cuando algo pediría permiso |
|---|---|
default (Manual) |
Se deniega, la ejecución continúa y termina en success |
acceptEdits |
Pasan las ediciones y los comandos de fichero comunes (touch sí, chmod no); el resto se deniega igual de callado |
dontAsk |
Se deniega inmediatamente, sin reintentos ni espera |
auto |
Decide el clasificador; se aborta a los 3 bloqueos seguidos o 20 en total |
bypassPermissions |
Se ejecuta, salvo las excepciones de la lista de arriba |
Si lo tuyo es la sesión interactiva y no el pipeline, el mapa es otro: los 6 modos de permisos con Shift+Tab para el día a día, el sandbox para ejecutar sin diálogos con red y disco acotados, y montar a Claude como agente autónomo si lo que quieres es dejarlo trabajando de madrugada.
Documentación oficial: Permission modes · Non-interactive mode · CLI reference
Requisitos: --permission-mode dontAsk y el resto de valores están en Claude Code v2.1.x; el alias manual para default pide v2.1.200 o superior. Las ejecuciones de este tip las hice en v2.1.220. Los topes del clasificador, el cortacircuitos de rm -rf y las dos negativas de arranque salen de la documentación vigente, no de mis pruebas.