Hooks de Claude Code / Ruta 04
Ruta 04 · Equipo & CI
Qué cambia cuando el hook sale de tu máquina: versionarlo para el equipo, confiar en el determinismo, ejecutarlo sin que nadie esté mirando (headless/CI) y tener cuidado para no lastimarte con el poder que tiene un hook.
Lecciones de esta ruta
settings.json en Git, todo el equipo hereda las mismas reglas, lo que queda fuera (local) y el orden de precedencia.
Idempotencia, timeouts, probar un hook aislado con stdin falso y fallar de forma abierta con un registro para depurar.
Modo headless, hooks en CI/pipeline, códigos de salida sin interacción y qué ocurre cuando termina un subagente.
Un hook se ejecuta con todos tus poderes: nunca confíes en la entrada, ten cuidado con PreCompact y sigue la lista final de seguridad.
Ruta 04 · Lección 1 · Avanzado
Un hook que solo existe en tu máquina no protege a nadie más que a ti; es hora de hacer que viaje con el repositorio.
↓ desplázate para estudiar
Hasta aquí, todos los hooks que configuraste vivían en tu máquina. Pero el .claude/settings.json es un archivo normal, dentro de la carpeta del proyecto —y un archivo normal puede ir a Git.
Cuando se versiona (se confirma con commit, con git add), deja de ser "el hook de Nei" y se convierte en el hook del proyecto. Empieza a viajar junto con el código, en el mismo commit que el bloqueo o el formateador que protege.
Pruébate
Para que un hook deje de ser solo tuyo y pase a ser del proyecto, ¿qué cambia?
Con el settings.json versionado, el efecto es automático: quien clona el repositorio o ejecuta git pull, recibe el archivo — y Claude Code lee los mismos hooks. Sin pedirle a nadie que instale nada ni pegar instrucciones en ningún README.
Esto cambia el criterio para decidir qué vale la pena automatizar. Un formateador o un bloqueo de seguridad que solo tienes tú te protege solo a ti. El mismo hook, versionado, protege todas las personas que toquen el código — incluso a quienes todavía no se han unido al equipo.
Pruébate
Si un colega clona el repositorio y el .claude/settings.json ya hay una restricción configurada, ¿qué sucede?
No todo es del equipo. Secretos, atajos personales, una ruta que solo existe en tu máquina: eso no debería viajar al repositorio. Para eso existe el settings.local.json: el mismo formato, la misma carpeta .claude/, pero aparece en la lista de .gitignore.
# queda fuera del repositorio
echo ".claude/settings.local.json" >> .gitignore
Regla práctica: si un hook debería aplicarse a cualquier persona que, al abrir este proyecto, irá al settings.json. Si es una preferencia tuya — o contiene algo sensible — va en el .local.json.
Pruébate
Quieres un ajuste personal, solo para ESTE proyecto, sin subirlo a Git. ¿Qué archivo?
Ahora tienes hasta tres archivos vigentes al mismo tiempo: el global (~/.claude/settings.json, todos tus proyectos), el de proyecto (.claude/settings.json, versionado) y el local (.claude/settings.local.json, solo tuyo, solo aquí).
En caso de conflicto, gana el más específico: la configuración local prevalece sobre la del proyecto, que prevalece sobre la global. Piénsalo así: la global es la regla general de tu casa; la del proyecto es la regla del edificio; la local es la nota adhesiva en tu puerta, y esa es la que prevalece.
Pruébate
Si hay un conflicto entre .claude/settings.json (proyecto) y .claude/settings.local.json (local), ¿cuál prevalece?
Esto se aplica herramienta por herramienta; no es todo o nada: si la configuración global tiene un hook en PostToolUse y el del proyecto tiene otro en PreToolUse, ambos conviven: la precedencia decide cuál prevalece cuando hay conflicto real en la misma regla.
Practica ahora 0/4 hecho
Haz que el estándar del equipo viaje con el repositorio de verdad. ~5 min, seguro: solo estás versionando un archivo de configuración, no ejecutando ningún comando destructivo.
# lo que es del equipo va a Git git add .claude/settings.json git status # lo que es solo tuyo queda fuera echo ".claude/settings.local.json" >> .gitignore git add .gitignore git commit -m "chore: versiona hooks do time"
✓ El estándar dejó de estar solo en tu cabeza (o solo en tu máquina): ahora está en el repositorio y viaja con él.
Ruta 04 · Lección 2 · Avanzado
Un hook que a veces se bloquea y a veces no es peor que no tener ningún hook; aquí aprenderás a confiar (y a probar) lo que automatizaste.
↓ desplázate para estudiar
Un hook es un comando común, y los comandos comunes no «encuentran» nada: es idempotente: ejecutar la misma entrada dos veces debe dar el mismo resultado, sin que nada falle la segunda vez.
Ejemplo práctico: un hook que formatea un archivo puede ejecutarse diez veces seguidas; el archivo formateado, aunque se vuelva a formatear, sigue igual. En cambio, un hook que suma algo en cada ejecución (como incrementar un contador sin comprobar si ya se ejecutó) no es idempotente — y ahí es donde viven los errores sutiles de automatización.
Pruébate
Un hook idempotente significa que:
timeoutUn hook se interpone en cada acción; si se bloquea, todo se bloquea juntos. Por eso cada hook tiene un timeout, en segundos:
{ "hooks": { "PreToolUse": [ {
"matcher": "Bash",
"hooks": [ { "type": "command", "command": "./guarda.sh",
"timeout": 10 } ]
} ] } }
Si no defines timeout, el patrón de un hook de comando es 600 segundos (10 minutos) — demasiado generoso para un bloqueo. Un hook rápido ni se acerca; pero un hook que hace una llamada de red sin cuidado puede dejar tu sesión colgada hasta que se agote el tiempo. Por eso conviene definir un límite corto (p. ej.: 10) en los hooks que intervienen en cada acción.
Pruébate
Si no defines el campo timeout en un hook de comando, ¿cuál es el patrón?
No necesitas Claude Code de verdad para saber si un hook funciona. Todos los hooks leen el mismo stdin — entonces simulas ese JSON a mano y se lo pasas directamente al hook:
# prueba aislada — sin abrir Claude Code
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
| bash .claude/hooks/guarda.sh
echo "Exit: $?"
Esto vale oro: ves exactamente lo que haría el hook, en tu terminal, en segundos, sin esperar una sesión entera solo para descubrir que olvidaste una comilla.
Un hook que falla por un error que no previste no debería bloquear todo tu trabajo. La regla es fallar de forma abierta: captura el error y deja que continúe.
# captura cualquier error inesperado y permite continuar set +e trap 'exit 0' ERR # ... lógica del hook ...
Y para depurar cuando algo sale mal, ejecútalo con -x (muestra cada línea ejecutada) y alimenta un archivo con el JSON de prueba:
bash -x .claude/hooks/guarda.sh < entrada.json echo "Exit: $?"
Pruébate
¿Cómo probar un hook sin que Claude tenga que activar el evento de verdad?
"Fallar abierto" (exit 0 ante un error inesperado) es diferente de "fallar bloqueando" (exit 2 a propósito). Lo primero es una red de seguridad contra errores en tu script; lo segundo es el bloqueo que diseñaste para que realmente bloquee.
Practica ahora 0/4 hecho
Dale a un hook un JSON falso y confirma que se comporta igual dos veces. ~5 min, seguro: nada de esto se acerca al Claude Code real.
#!/usr/bin/env bash # .claude/hooks/testa.sh set +e trap 'exit 0' ERR INPUT=$(cat) CMD=$(printf '%s' "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null) echo "recebi: $CMD" >> .claude/test.log exit 0
✓ Probaste un hook sin necesitar una sesión completa de Claude Code y viste con tus propios ojos cómo funciona el determinismo.
Ruta 04 · Lección 3 · Avanzado
No hay nadie frente a la pantalla, pero eso no significa que los hooks se tomen un descanso: también mantienen el estándar en modo headless y en CI.
↓ desplázate para estudiar
claude -pClaude Code también se ejecuta sin ninguna interfaz: el modo headless, activado por un script: claude -p "seu comando aqui". Lee el prompt, trabaja, responde y termina. Nada de pantalla ni de que tú tengas que mirar.
Este es el modo que se usa en un script de deploy, en un cron o dentro de un pipeline. Y justamente ahí los hooks demuestran su valor.
Si el settings.json está versionado (Lección 1), se aplica en cualquier lugar que ejecute Claude Code, incluso dentro de un runner de CI, sin nadie conectado y sin ninguna pantalla.
El bloqueo que impide rm -rf, el formateador que corrige el código, el registro que anota los comandos: todo eso sigue activo. El mismo patrón que te protege en el día a día protege al robot también.
Pruébate
En modo headless (claude -p) y dentro de un pipeline de CI, los hooks configurados:
Sin nadie en pantalla, el hook no puede detenerse para preguntar. Los hooks no son interactivos: no esperes que una persona escriba «sí» a mitad de la ejecución. Todo se resuelve mediante PreToolUse y por el código de salida:
PreToolUse e Stop); o stderr se convierte en el motivo.En CI esto es aún más importante: si un hook intentara «preguntar», el pipeline se quedaría bloqueado para siempre esperando una respuesta que nunca llega.
Pruébate
¿Un hook puede pausar la ejecución y esperar a que escribas una respuesta en medio del proceso?
Además del exit code, un PreToolUse puedes devolver JSON con permissionDecision: allow/deny/ask. Pero ask no tiene sentido en headless: no hay quién responda. En CI, prefiere siempre allow/deny explícitos.
Existe un evento parecido a Stop, pero para un nivel inferior: SubagentStop se activa cuando un subagente termina su propio turno; no el del agente principal, sino el de una tarea delegada.
Diferencias importantes: SubagentStop no bloquea (a diferencia de Stop, que acepta exit 2), y no usa matcher. Trátalo como un aviso («esta parte terminó»), no como un bloqueo — y no construyas lógica crítica basándote solo en él, ya que su comportamiento puede llegar incompleto.
Pruébate
¿Qué evento se activa cuando un SUBAGENTE (no el agente principal) termina su turno?
Practica ahora 0/4 hecho
Comprueba que el hook se active incluso sin ninguna pantalla. ~5 min, seguro: el comando solo lista archivos.
# el hook de registro que ya tienes en .claude/settings.json sigue vigente cat .claude/settings.json # ejecuta Claude Code sin ninguna interfaz claude -p "liste os arquivos deste diretório" # comprueba: el hook se activó incluso en modo headless cat .claude/test.log
✓ El mismo hook que protege tu día a día protegió la automatización — sin ninguna línea adicional de configuración.
Ruta 04 · Lección 4 · Avanzado
Un hook se ejecuta con todo tu poder, y el poder sin cuidado es la receta perfecta para un accidente costoso.
↓ desplázate para estudiar
Un hook no es un sandbox ni un script «de juguete»; se ejecuta con los los mismos permisos que tu usuario: el mismo shell, las mismas credenciales, el mismo acceso a la red que tienes al abrir la terminal.
Esto no es un defecto, es la razón por la que funcionan los hooks: para formatear un archivo o bloquear un rm -rf, el hook debe poder interactuar con el sistema de verdad. Pero ese mismo poder que protege también puede, si está mal escrito, estropear — entonces trata el hook con el respeto que merece un script de acceso total.
Pruébate
Al ejecutarse, un hook tiene acceso a:
O tool_input que llega a tu hook proviene de un contexto que no controlas por completo. Poner ese valor directamente dentro de un comando shell es la puerta de entrada clásica para inyección de shell:
# peligroso: interpola directamente en el shell cmd=$(jq -r '.tool_input.command') eval "$cmd" ← NO hagas esto # mejor: trátalo como dato, valídalo antes de actuar cmd=$(jq -r '.tool_input.command') echo "$cmd" | grep -qE 'rm -rf|:\(\)\{' && { echo "bloqueado" >&2; exit 2; } exit 0
El mismo cuidado se aplica a las rutas de archivos: siempre valida (sin ../ saliendo de la carpeta del proyecto) antes de leer o escribir: path traversal es el mismo problema, pero con archivos en vez de comandos. Y protege a propósito lo que es sensible: .env, .ssh, .git, claves privadas.
Pruébate
¿Por qué NUNCA se debe interpolar tool_input directamente dentro de un comando shell (como eval "$cmd")?
Existe un evento llamado PreCompact, vinculado al momento en que se compacta el contexto de la conversación. La idea es darte una oportunidad para guardar o limpiar el estado antes de que ocurra.
Pero es uno de los eventos menos documentados oficialmente — no des por sentadas garantías sólidas sobre exactamente cuándo o cómo se activa. En la práctica: úsalo para cosas no críticas (un registro, una copia de seguridad), siempre teniendo el mismo cuidado de fallar de forma abierta, y nunca apuestes la seguridad del proyecto solo a eso.
Antes de considerar que un hook está «listo para el equipo», revisa esta lista:
timeout definido.eval de cadena sin procesar, rutas validadas..env, .ssh, .git, claves fuera del alcance de un hook mal escrito.settings.json (está en una variable de entorno o en un archivo).exit 0 con registro, no bloquea a Claude.Empieza poco a poco y refuerza las medidas con el tiempo. El hook que sobrevive en tu equipo es el que nadie nota, porque nunca estorba y siempre protege.
Vale la pena revisar los hooks de vez en cuando, como quien revisa los permisos de acceso: lo que era necesario hace 3 meses quizá ya no lo sea, y cada línea de comando automático es una línea que se ejecuta sin que tú lo mires — la esencia de toda esta ruta.
Practica ahora 0/4 hecho
Revisa el settings.json del proyecto con la lista de seguridad: comillas correctas, validación, sin secretos en el comando. ~10 min, seguro: es una revisión, no se ejecuta nada destructivo.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guarda.sh\"",
"timeout": 10
}
]
}
]
}
}✓ Lista de verificación completa: comillas correctas, sin secretos en el comando, timeout definido, el script falla en modo abierto. Eso es madurez: un hook confiable es un hook que nunca te perjudica. Fin de la ruta. 🎉