PTENES
INEMA.CLUBPROHooks de Claude Code

Hooks de Claude Code / Ruta 04

Ruta 04 · Equipo & CI

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.

4lecciones
~36minutos
avanzadonivel

Ruta 04 · Lección 1 · Avanzado

Versionar
pro equipo

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

01 settings.json en Git

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?

02 Todo el equipo hereda las mismas reglas

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?

03 settings.local.json: fuera de Git

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?

04 Precedencia: user → project → local

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?

Profundiza

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

Confirma el settings.json del proyecto

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

Determinismo:
se puede confiar

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

01 Idempotencia: ejecutarlo dos veces produce el mismo

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:

02 Tiempos de espera: el campo timeout

Un 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?

03 Probar de forma aislada: stdin falso

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.

04 Fail-open + registrar para depurar

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?

Profundiza

"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

Prueba un hook con stdin falso

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

Ejecutando sin
nadie mirando

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

01 Modo headless: claude -p

Claude 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.

02 Hooks en CI/pipeline

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:

03 Exit codes y falta de interactividad

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:

  • exit 0 — continúa con normalidad.
  • exit 2 — bloquea (solo aplica en PreToolUse e Stop); o stderr se convierte en el motivo.
  • exit 1, 3+ — un error no bloqueante; Claude continúa (fail-open).

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?

Profundiza

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.

04 SubagentStop: cuando termina un subagente

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

Ejecuta Claude con -p y un hook activo

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

Higiene:
hook es poder

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

01 Los hooks se ejecutan con TODOS tus poderes

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:

02 Nunca confíes en el input

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")?

03 PreCompact y limpieza del estado

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.

04 Lista final de seguridad

Antes de considerar que un hook está «listo para el equipo», revisa esta lista:

  • Rápido — no bloquea el flujo; tiene timeout definido.
  • Idempotente — ejecutarlo dos veces no rompe nada.
  • Nunca confía en el input — sin eval de cadena sin procesar, rutas validadas.
  • Protege lo sensible — .env, .ssh, .git, claves fuera del alcance de un hook mal escrito.
  • Sin secretos en el comando — nada de escribir el token o la contraseña directamente en settings.json (está en una variable de entorno o en un archivo).
  • Fallar en modo abierto — un error inesperado se convierte en 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.

Profundiza

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

Refuerza tu settings.json · capstone

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. 🎉