Hooks en Claude Code: automatiza y aplica reglas

Los hooks de Claude Code son comandos de shell que se ejecutan automáticamente en puntos concretos de su ciclo de vida. Su gran diferencia con el CLAUDE.md es que el hook siempre se ejecuta: no depende de que el modelo decida hacerlo. Por eso son la herramienta para forzar reglas (formatear, bloquear, validar), no solo sugerirlas.
En este artículo verás qué son, qué eventos existen y cómo configurarlos, con ejemplos aplicados a un proyecto Laravel (formatear con Pint, proteger tu .env, no cerrar una tarea con los tests en rojo) y una tabla de qué regla va en el CLAUDE.md y cuál en un hook.
Por qué un hook y no una instrucción
El CLAUDE.md es guía: Claude lo lee y trata de seguirlo, pero no hay garantía. Un hook es determinista: corre como un comando del sistema en el evento que definas, decida lo que decida el modelo. Si algo tiene que pasar (formatear tras cada edición, no tocar ciertos archivos), va en un hook.
Los eventos principales
Claude Code expone muchos eventos de ciclo de vida (más de treinta). Estos son los que más vas a usar:
PreToolUse: antes de que Claude use una herramienta (puedes bloquear la acción).PostToolUse: después de usar una herramienta (ideal para formatear o lintar tras editar).UserPromptSubmit: cuando envías un prompt.Notification: cuando Claude espera tu input o permiso.SessionStart: al iniciar la sesión (inyectar contexto).Stop/SubagentStop: cuando Claude termina de responder o cuando termina un subagente.
Cómo se configuran
Los hooks viven en un archivo de settings: ~/.claude/settings.json (global), .claude/settings.json (del proyecto) o .claude/settings.local.json (local, sin commitear). La estructura es: evento → lista de entradas con un matcher (el patrón que decide qué herramienta dispara el hook: una cadena exacta, una lista separada por |, o una expresión regular) y los hooks a ejecutar.
Ejemplo genérico: formatear con Prettier cada vez que Claude escribe o edita un archivo. El matcher Write|Edit calza con ambas herramientas.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$(jq -r '.tool_input.file_path')\"" }
]
}
]
}
}El hook recibe la información del evento como JSON por stdin: la ruta del archivo recién editado viene en tool_input.file_path, que extraes con jq. Por eso el comando de arriba lee esa ruta con jq -r '.tool_input.file_path' y se la pasa a Prettier. (No hay una variable de entorno con las rutas; en un hook de tipo command el dato siempre llega por stdin.)
Hooks para un proyecto Laravel
Tres ejemplos de control de calidad automático:
1. Formatear con Pint tras cada edición. En vez de recordarle a Claude que respete el estilo, lo fuerzas: cada Write|Edit pasa por Pint.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "./vendor/bin/pint \"$(jq -r '.tool_input.file_path')\"" }
]
}
]
}
}2. Proteger archivos sensibles. Un hook en PreToolUse puede denegar una edición antes de que ocurra. Aquí bloqueamos cualquier cambio a tu .env o a las migraciones ya versionadas. Apunta el hook a un script:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/proteger-laravel.sh" }
]
}
]
}
}Y el script lee la ruta del archivo y decide. Para bloquear, imprime un JSON con permissionDecision: "deny":
#!/usr/bin/env bash
ruta=$(jq -r '.tool_input.file_path')
if [[ "$ruta" == *".env"* || "$ruta" == *"/database/migrations/"* ]]; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Archivo protegido: edítalo tú a mano."}}'
fi3. No cerrar una tarea con los tests en rojo. Stop se dispara cuando Claude termina de responder. Si el hook sale con código 2, Claude no se detiene: recibe lo que escribiste en stderr como el motivo para seguir trabajando. Con eso, una tarea solo se cierra cuando PHPStan (o Larastan) y la suite pasan:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": ".claude/hooks/verify-laravel.sh" }
]
}
]
}
}#!/usr/bin/env bash
# No PHP changes in the working tree: nothing to verify
if [[ -z "$(git status --porcelain -- '*.php')" ]]; then
exit 0
fi
if ! output=$(./vendor/bin/phpstan analyse --no-progress --error-format=raw 2>&1); then
echo "PHPStan falla. Corrígelo antes de terminar:" >&2
echo "$output" | tail -n 30 >&2
exit 2
fi
if ! output=$(php artisan test 2>&1); then
echo "Hay tests en rojo. Corrígelos antes de terminar:" >&2
echo "$output" | tail -n 40 >&2
exit 2
fiEl primer if evita correr la suite en cada respuesta: si no hay archivos PHP modificados, Claude termina sin más. El tail recorta la salida para no llenar el contexto con cientos de líneas. Y no hace falta temer un bucle infinito: Claude Code corta tras ocho continuaciones seguidas forzadas por hooks de Stop (el contador vuelve a cero cada vez que Claude usa una herramienta), y el JSON de entrada trae stop_hook_active: true cuando Claude ya está siguiendo por un hook, por si quieres cortar antes.
Puedes abrir /hooks para ver los eventos y los hooks configurados (es de solo lectura: para editarlos, tocas el JSON o le pides a Claude que lo haga). Este flujo encaja con el resto del cluster de IA para desarrolladores Laravel y con usar Claude Code en un proyecto Laravel real.
Que Claude no edite sus propios controles
Un hook que el agente puede reescribir no protege nada. Si los tests fallan, la salida fácil para el modelo es tocar el phpstan.neon, subir errores al baseline o editar el script del hook. Para cerrar esa puerta, añade reglas deny de Edit en .claude/settings.json:
{
"permissions": {
"deny": [
"Edit(/.claude/hooks/**)",
"Edit(/phpstan.neon)",
"Edit(/phpstan-baseline.neon)",
"Edit(/pint.json)"
]
}
}La / inicial ancla la ruta a la raíz del proyecto. Además, .claude es una ruta protegida: en los modos default y acceptEdits Claude Code siempre te pide permiso antes de escribir ahí, aunque en bypassPermissions lo escribe sin preguntar. Las reglas deny cubren las herramientas de archivos y los comandos de Bash que Claude Code reconoce (sed, tee, redirecciones), pero no un script de PHP o Node que abra el archivo por su cuenta. Si necesitas que eso tampoco pase, la documentación remite al sandbox.
CLAUDE.md pide, el hook obliga: qué regla va dónde
El CLAUDE.md es una petición: Claude lo lee y casi siempre lo cumple, pero puede no hacerlo. Si romper la regla cuesta caro, tiene que haber algo que pueda negarse. El blog de Frugäast lo resume en el título de un artículo, "Prompts ask, hooks enforce". La regla práctica es elegir el mecanismo según lo que cuesta que la regla falle:
| Tipo de regla | Ejemplo en Laravel | Dónde va |
|---|---|---|
| Preferencia o convención | "Usa Form Requests para validar" | CLAUDE.md |
| Procedimiento de varios pasos | Cómo crear un módulo nuevo | Skill |
| Tarea ruidosa que ensucia el contexto | Revisar todo un directorio de logs | Subagente |
| Algo que debe pasar siempre | Formatear con Pint tras editar | Hook PostToolUse |
| Algo que no debe pasar nunca | Editar el .env o una migración ya versionada |
Hook PreToolUse o regla deny |
| Condición para dar la tarea por terminada | PHPStan y tests en verde | Hook Stop, y CI como red final |
Si Claude ignora una línea de tu CLAUDE.md una vez, reescribirla con mayúsculas no la vuelve obligatoria. Pregúntate si un script podría impedirlo y, si puede, muévela a un hook. Para ver cómo se combinan todas las piezas, revisa la guía de Claude Code.
Preguntas frecuentes
¿Qué son los hooks en Claude Code?
Comandos de shell que Claude Code ejecuta automáticamente en eventos de su ciclo de vida (antes/después de usar una herramienta, al notificar, al iniciar sesión, etc.) para automatizar tareas y aplicar reglas de forma determinista.
¿Dónde se configuran los hooks?
En un archivo de settings (~/.claude/settings.json, .claude/settings.json del proyecto o .claude/settings.local.json), dentro de un bloque hooks, agrupados por evento, con un matcher y los comandos a ejecutar.
¿Un hook puede impedir una acción de Claude?
Sí. Un hook en PreToolUse puede denegar el uso de una herramienta antes de que ocurra: imprime un JSON con permissionDecision: "deny" (o termina con código de salida 2). Es la forma de proteger archivos como tu .env.
¿Cómo formateo con Pint automáticamente?
Con un hook PostToolUse (matcher Write|Edit) que corra Pint sobre el archivo recién editado. La ruta llega en el JSON del evento por stdin, así que la extraes con jq: ./vendor/bin/pint "$(jq -r '.tool_input.file_path')".
¿Cuándo uso un hook en vez del CLAUDE.md?
Cuando algo debe ocurrir siempre en un momento exacto. El CLAUDE.md es guía (puede no cumplirse); el hook es enforcement.
¿Por qué Claude Code no respeta mi CLAUDE.md?
Porque el CLAUDE.md entra al contexto como instrucción, y el modelo la sigue casi siempre, no siempre. Si la regla tiene que cumplirse sí o sí, conviértela en un hook (PreToolUse para impedir algo, PostToolUse para forzarlo, Stop para exigirlo antes de terminar) o en una regla deny de permisos.
¿Cómo hago que Claude Code corra los tests antes de terminar?
Con un hook en Stop que corra php artisan test y salga con código 2 si fallan. Claude recibe el stderr del script como motivo para seguir trabajando y no cierra la tarea hasta que la suite pase o se alcance el tope de ocho continuaciones seguidas.
Fuentes
- Hooks reference (documentación oficial de Claude Code)
- Configure permissions (documentación oficial de Claude Code)
- Permission modes: protected paths (documentación oficial de Claude Code)
- Prompts ask, hooks enforce (blog de Frugäast)
¿Tienes un proyecto en mente?
Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.