
Hooks en Claude Code: checks automáticos sin tocar el flujo
TL;DR
- Los hooks de Claude Code son comandos shell que se disparan en eventos del agente (antes o después de usar una tool, al recibir un prompt, al cerrar sesión).
- Permiten correr linters, formateadores, validaciones o bloqueos sin meter ruido en el prompt.
- Se configuran en
settings.jsona nivel proyecto o global y conviven con permisos y MCPs.
Por qué los hooks importan en tu flujo diario
Cuando trabajo con Claude Code en un repo real, hay tareas que repito en cada sesión: pasar el formateador después de un Edit, validar que no se cuele un console.log, o evitar que el agente ejecute comandos destructivos sobre node_modules. Antes lo hacía recordándoselo en el prompt, lo cual es frágil: si la sesión se compacta o cambio de modelo, el contexto se pierde.
Los hooks resuelven esto moviendo esos chequeos fuera del prompt: el harness los ejecuta como código, no como instrucción al modelo. Es la diferencia entre pedirle a un compañero que recuerde formatear el código y tener un pre-commit hook que lo hace siempre.
Si vienes de configurar memoria y permisos, esto encaja en la misma capa de setup serio que ya cubrí en Claude Code: Memoria, MCPs y Mapa de Repo para Menos Tokens.
¿Qué es un hook en Claude Code?
Un hook es un comando shell que se ejecuta automáticamente cuando ocurre un evento del agente. Recibe información del evento por stdin en formato JSON, puede modificar el comportamiento (bloquear, advertir, inyectar contexto) y devuelve un exit code que decide si la acción continúa.
Es código tuyo corriendo en tu máquina, no parte del prompt. El modelo no lo ve a menos que tú decidas devolverle algo por stdout.
Eventos disponibles (los que uso de verdad)
| Evento | Cuándo dispara | Caso típico |
|---|---|---|
PreToolUse | Antes de ejecutar una tool (Bash, Edit, Write...) | Bloquear comandos peligrosos, validar paths |
PostToolUse | Después de ejecutar una tool | Formatear código tras Edit, correr linter |
UserPromptSubmit | Cuando envías un mensaje al agente | Inyectar contexto del repo, normalizar prompts |
Stop | Cuando el agente termina su respuesta | Resumen de cambios, notificación |
SessionStart | Al abrir sesión | Cargar variables, mostrar estado del repo |
Hay más, pero estos cinco cubren el 90% de lo que vas a querer hacer.
Configuración paso a paso
1. Localiza tu settings.json
Tienes dos niveles:
- Global:
~/.claude/settings.json, aplica a todas las sesiones. - Proyecto:
.claude/settings.jsonen la raíz del repo, versionable.
Para hooks que dependen del stack del proyecto (Prettier, ESLint, Black, Ruff...), usa el del proyecto. Para reglas de seguridad personales, el global.
2. Define tu primer hook: formateo automático tras editar
Este hook lanza Prettier sobre cada archivo que el agente edita o crea, sin que tú lo pidas.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I {} npx prettier --write {} 2>/dev/null || true"
}
]
}
]
}
}
Qué hace: lee el JSON del evento por stdin, extrae la ruta del archivo y lanza Prettier. El || true evita que un fallo del formateador rompa el flujo del agente.
3. Añade un guardrail con PreToolUse
Bloquea cualquier rm -rf antes de que se ejecute. Devolver exit code 2 en un PreToolUse cancela la acción y devuelve el mensaje al agente.
#!/usr/bin/env bash
# Bloquea rm -rf en cualquier path; devuelve mensaje al agente
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -qE 'rm\s+-rf'; then
echo "Bloqueado: rm -rf no permitido. Usa trash o confirma manualmente." >&2
exit 2
fi
exit 0
Guarda el script como .claude/hooks/block-rm.sh, dale permisos (chmod +x) y referénciálo desde settings.json con matcher: "Bash".
4. Verifica que dispara
Pídele al agente algo trivial ("edita el README y añade una línea") y observa la consola. Si Prettier corrió, verás el archivo formateado al instante. Si no, revisa el siguiente apartado de errores.
Caso real: linting silencioso en un repo Python
En un proyecto FastAPI tenía dos problemas: el agente generaba imports desordenados y a veces dejaba print() de debug. La solución fue un PostToolUse con dos comandos encadenados:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep '\\.py$' | xargs -I {} sh -c 'ruff check --fix {} && ruff format {}' 2>/dev/null || true"
}
]
}
]
}
}
Resultado: el código sale del agente ya formateado y con imports ordenados. Sin recordatorios en el prompt, sin pasos manuales. Es el mismo principio que aplico cuando uso slash commands en Claude Code para automatizar tareas: bajar el coste cognitivo de cada sesión.
En Producción
Rendimiento y timeouts
Cada hook bloquea el flujo del agente hasta que termina. Si tu linter tarda 8 segundos, vas a notarlo en cada Edit. Reglas que aplico:
- Hooks incrementales: corre Prettier o Ruff solo sobre el archivo afectado, no sobre todo el repo.
- Timeout explícito: envuelve comandos lentos en
timeout 5s ...para evitar bloqueos largos. - Evita tests completos en
PostToolUse. Para eso, mejor unStopque corre una vez al final de la respuesta.
Costes
Los hooks no consumen tokens del modelo (es código local), pero si devuelves contenido al agente vía stdout en eventos como UserPromptSubmit o PreToolUse, ese texto sí entra al contexto. Mantén las salidas cortas y verifica que no estás duplicando información que ya está en CLAUDE.md.
Seguridad
Un settings.json en el repo puede ejecutar comandos arbitrarios al abrir el proyecto. Si clonas un repo desconocido, revisa .claude/ antes de abrir Claude Code. Es el mismo riesgo que con scripts de Husky o pre-commit. La capa de protección complementa lo que cubrí en guardrails en Claude Code.
Versionado
El .claude/settings.json del proyecto debería ir al repo para que el equipo comparta los mismos hooks, igual que se versiona un .eslintrc. El global queda en tu máquina. No mezcles secretos en hooks: usa variables de entorno.
Errores comunes
Error: el hook no dispara nunca. Causa: el matcher no coincide con el nombre exacto de la tool. Solución: usa "Edit|Write" con regex o "*" para todas; revisa la documentación oficial para los nombres exactos en tu versión de Claude Code.
Error: el agente se queda colgado tras un Edit. Causa: el comando del hook no termina o pide input interactivo. Solución: redirige stdin (< /dev/null), añade timeout y siempre cierra con || true si el fallo no debe romper el flujo.
Error: jq: command not found al disparar el hook. Causa: el hook corre con tu shell pero sin tu PATH completo. Solución: usa rutas absolutas (/usr/bin/jq) o exporta el PATH en un SessionStart.
Error: el guardrail bloquea pero el agente no entiende por qué. Causa: el mensaje va a stdout en lugar de stderr, o el exit code no es 2. Solución: imprime a stderr con echo "..." >&2 y devuelve exit 2 para que el agente reciba el feedback.
Preguntas Frecuentes
¿Los hooks de Claude Code reemplazan a los pre-commit hooks de git?
No, son complementarios. Los hooks de Claude Code corren durante la sesión del agente, antes de que el código llegue a un commit. Los pre-commit de git corren al hacer git commit. Lo ideal es tener ambos: el primero acelera el feedback durante la generación, el segundo es la red de seguridad final.
¿Puedo bloquear el uso de ciertas tools sin tocar permisos?
Sí. Un PreToolUse con matcher sobre la tool y exit 2 bloquea esa llamada concreta y devuelve un mensaje al agente. Es más expresivo que los permisos básicos porque puedes condicionar el bloqueo al contenido del comando, no solo al nombre de la tool.
¿Funcionan los hooks con subagentes?
Sí, los hooks aplican al harness completo. Cuando un subagente ejecuta una tool, los PreToolUse y PostToolUse también disparan. Es útil para mantener la misma política de formateo y bloqueos en flujos paralelos.
Cierre
Los hooks son la pieza menos vistosa del setup de Claude Code y, en mi experiencia, la que más reduce fricción a medio plazo. Mover el formateo, las validaciones y los bloqueos fuera del prompt te deja sesiones más cortas, más reproducibles y con menos drift entre lo que pides y lo que el agente entrega. Si ya tienes permisos y memoria configurados, esto es el siguiente paso natural antes de complicarte con orquestación o subagentes.
¿Tienes algún hook que te haya salvado el día? Cuéntamelo en Twitter en @sergiomarquezp_. En el próximo artículo voy a entrar en cómo combinar hooks con MCPs para dejar que el agente reaccione a eventos externos sin perder control.


