Hooks de Claude Code: cómo convertir las reglas del agente en garantías
Los hooks de Claude Code son comandos de shell que el propio Claude Code ejecuta en determinados eventos: antes y después de llamar a una herramienta, al enviar un prompt, al iniciar la sesión y antes de que el agente se detenga. Se configuran en settings.json, y el código de salida 2 bloquea la acción y devuelve al agente un mensaje de error; así el hook convierte una regla de CLAUDE.md en una garantía que el modelo no puede olvidar.
Qué son los hooks de Claude Code
Un hook es un comando que no lanza el modelo, sino el propio programa Claude Code en el momento concreto del trabajo del agente. El agente está a punto de ejecutar rm -rf: el hook se activa y decide si deja pasar el comando. El agente ha editado un archivo: el hook ejecuta el formateador. El agente quiere terminar: el hook lanza las pruebas y no le deja parar mientras estén en rojo.
La diferencia principal con las instrucciones de CLAUDE.md es esta: una instrucción es un texto que el modelo tiene en cuenta con cierta probabilidad, mientras que un hook es código determinista. Se ejecuta siempre, independientemente de la longitud del contexto, de lo que el agente haya «decidido» o de si olvidó la regla en el minuto cien de la sesión. Por eso los hooks de Claude Code son la forma principal de convertir un deseo en una garantía.
Por qué las reglas en CLAUDE.md no bastan
Una situación típica: el agente terminó una función, las pruebas unitarias están en verde y en el chat aparece un seguro «Listo». Pero nadie ejecutó el escenario de extremo a extremo, el linter no se pasó y la migración falló a mitad. En CLAUDE.md, sin embargo, está escrito «antes de terminar, ejecuta las pruebas». El modelo no engaña a propósito: simplemente se evalúa a sí mismo, y esa autoevaluación no es fiable.
De ahí el principio del harness: todo lo que se puede comprobar mecánicamente lo comprueba la mecánica, no el modelo. La regla «ejecuta el linter» se convierte en un hook que ejecuta el linter. La regla «no toques la configuración de producción» se convierte en un hook que bloquea la escritura en ese archivo. La instrucción de texto se mantiene, pero solo explica al agente por qué lo detuvieron.
Eventos: cuándo se activan los hooks
Cada hook está ligado a un evento del ciclo de vida de la sesión. Los principales eventos:
| Evento | Cuándo se activa | Uso típico |
|---|---|---|
PreToolUse | antes de llamar a una herramienta (Bash, Edit, Write, etc.) | bloquear comandos peligrosos, proteger archivos |
PostToolUse | después de una llamada correcta a una herramienta | formateador, linter, pruebas rápidas tras una edición |
UserPromptSubmit | cuando el usuario envía un prompt, antes de procesarlo | añadir contexto, rechazar un prompt con un secreto |
Stop | cuando el agente está a punto de terminar la respuesta | no dejarlo parar mientras no pasen las comprobaciones |
SubagentStop | cuando termina un subagente | revisar el resultado del subagente |
SessionStart | al iniciar o reanudar una sesión | cargar el estado: git status, el plan, el diario de progreso |
SessionEnd | al terminar la sesión | limpieza, escritura de registros |
Notification | cuando Claude Code envía una notificación (por ejemplo, espera un permiso) | sonido o mensaje en el teléfono |
PreCompact | antes de comprimir el contexto | guardar lo importante antes de la compactación |
Los más útiles para la fiabilidad son PreToolUse, PostToolUse y Stop: cubren tres puntos débiles del agente: las acciones peligrosas, las ediciones sin comprobar y el «listo» prematuro.
Dónde se configuran los hooks: settings.json
Los hooks se describen en los archivos de configuración de Claude Code:
~/.claude/settings.json: personales, para todos los proyectos;.claude/settings.jsonen el repositorio: compartidos por el equipo, se guardan en git;.claude/settings.local.json: locales del proyecto, no entran en git.
La estructura es: evento → lista de grupos con matcher → lista de comandos.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
]
}
}
matcher es el nombre de una herramienta o una expresión regular sobre ese nombre: Bash, Edit|Write, .*. Hace falta en los eventos relacionados con herramientas; para Stop o UserPromptSubmit puede omitirse. La variable CLAUDE_PROJECT_DIR apunta a la raíz del proyecto, así que conviene guardar los scripts en .claude/hooks/ y llamarlos a través de ella. Para ver los hooks activos, use el comando /hooks dentro de Claude Code.
Cómo el hook recibe los datos y responde
Entrada: JSON por stdin
Claude Code pasa al hook un JSON con los datos del evento: el identificador de la sesión, el directorio de trabajo, el nombre del evento y, para las herramientas, tool_name y tool_input. En Bash, tool_input.command contiene el comando; en Edit y Write, tool_input.file_path contiene la ruta del archivo. Es cómodo leerlo con jq.
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build/" },
"cwd": "/home/dev/project"
}
Salida: código de retorno
| Código | Qué ocurre |
|---|---|
0 | éxito, el trabajo continúa; en UserPromptSubmit y SessionStart la salida stdout se añade al contexto del agente |
2 | error bloqueante: stderr se entrega al agente y la acción se detiene (en PreToolUse, la llamada a la herramienta se cancela; en Stop, el agente sigue trabajando) |
| otro | error no bloqueante: stderr se muestra al usuario y el trabajo continúa |
El código 2 es la mecánica clave. El agente recibe el texto de stderr como retroalimentación y puede corregirse: reescribir el comando, arreglar la prueba, no tocar el archivo. Por eso en stderr conviene escribir no «error», sino el motivo claro y qué hacer en su lugar.
Además del código de retorno, el hook puede imprimir en stdout un JSON con la decisión, por ejemplo "decision": "block" y "reason". Sirve para ajustes finos, pero para la mayoría de tareas basta con el código 2 y un mensaje en stderr.
Ejemplo 1: linter y pruebas tras cada edición
Un hook en PostToolUse con el matcher Edit|Write se activa después de modificar un archivo. La herramienta ya se ejecutó y la edición no se puede deshacer, pero el código 2 devuelve al agente la salida del linter, y lo corrige en ese momento, no dentro de media hora.
#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
if ! out=$(ruff check "$file" 2>&1); then
echo "El linter encontró errores en $file:" >&2
echo "$out" >&2
exit 2
fi ;;
*.php)
if ! out=$(php -l "$file" 2>&1); then
echo "$out" >&2
exit 2
fi ;;
esac
exit 0
No ponga aquí una ejecución completa de las pruebas de diez minutos: el hook se activa en cada edición. Tras una edición, use comprobaciones rápidas (sintaxis, linter, pruebas de un módulo), y las pesadas déjelas para el hook Stop.
Ejemplo 2: bloqueo de comandos peligrosos
Un hook en PreToolUse con el matcher Bash ve el comando antes de ejecutarlo. El código 2 cancela la llamada y el agente recibe la explicación.
#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
echo "Comando bloqueado por la política del proyecto: $cmd. Las operaciones peligrosas las ejecuta una persona." >&2
exit 2
fi
exit 0
Así también se protegen los archivos: un hook en Edit|Write revisa file_path y bloquea la escritura en .env, en configuraciones de producción, en código generado o en migraciones ya aplicadas. Tenga en cuenta que una búsqueda por expresión regular se puede esquivar: el agente puede escribir el comando de otra forma. El hook es un buen seguro, pero no sustituye a los permisos de acceso ni al sandbox.
Ejemplo 3: el agente no puede decir «listo» mientras las pruebas estén en rojo
El hook Stop se activa cuando el agente está a punto de terminar. Si las pruebas no pasan, el código 2 impide que se detenga, y la salida de las pruebas le llega al agente como la siguiente tarea. Es la versión mecánica de la Definition of Done.
#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# protección contra bucles infinitos: el hook ya prolongó el trabajo una vez
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! out=$(npm test --silent 2>&1); then
echo "Las pruebas no pasaron, el trabajo no está terminado:" >&2
echo "$out" | tail -n 40 >&2
exit 2
fi
exit 0
El campo stop_hook_active vale true si el agente ya continúa trabajando por un hook de parada. Sin esta comprobación, el hook puede hacer que el agente dé vueltas sin fin: justo la situación en la que el agente «trabaja toda la noche en la dirección equivocada». En los ciclos autónomos añada además un límite de intentos y una condición en la que se avise a una persona.
Ejemplo 4: cargar el estado al inicio de la sesión
En SessionStart, la salida stdout del hook llega al contexto. Es cómodo mostrar la rama actual, los cambios sin confirmar y la siguiente tarea del plan, para que el agente no empiece desde cero.
#!/usr/bin/env bash
echo "Rama: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0
Cómo los hooks convierten las reglas en garantías
Conviene dividir las reglas del harness en tres niveles. El primero es el texto en CLAUDE.md: explica cómo trabajar, pero se cumple con probabilidad. El segundo son las comprobaciones ejecutables: pruebas, E2E, reglas de arquitectura en forma de script que falla si hay una infracción. El tercero son los hooks, que lanzan esas comprobaciones automáticamente en el momento oportuno, sin intervención del modelo.
Regla práctica: si el agente infringió dos veces la misma regla, no añada otra advertencia en mayúsculas a CLAUDE.md. Pregúntese si la infracción se puede verificar con un script. Si se puede, cree el script y engánchelo a un hook. Así la instrucción de texto se hace más corta y el comportamiento más estable.
| Regla en el texto | Garantía mediante hook |
|---|---|
| «Formatea el código según el estándar» | PostToolUse ejecuta el formateador sobre el archivo modificado |
| «No hagas push con force» | PreToolUse en Bash bloquea el comando, exit 2 |
| «No digas listo mientras las pruebas estén en rojo» | Stop ejecuta las pruebas y no deja terminar |
| «No edites .env» | PreToolUse en Edit|Write comprueba la ruta |
| «Recuerda en qué te quedaste» | SessionStart muestra el plan y git status |
Errores típicos
- Código 1 en lugar de 2. El script termina con código 1, el agente no se entera y la acción sigue adelante. Solo el código 2 bloquea.
- Mensaje en stdout. Con código 2, al agente le llega stderr. Escriba con
>&2. - Hook lento en un evento frecuente. Una ejecución completa de las pruebas en PostToolUse hace que cada edición tarde un minuto. Lo pesado va en Stop.
- Stop sin fin. Sin comprobación de
stop_hook_activeni límite, el agente da vueltas sin parar. - Rutas relativas. El hook se ejecuta en el directorio de trabajo de la sesión; use
$CLAUDE_PROJECT_DIR. - Hook ajeno sin revisar. El hook se ejecuta con sus permisos y sin confirmación. Lea los scripts de repositorios ajenos antes de confiar en ellos.
Resumen
Los hooks de Claude Code trasladan las reglas más importantes del texto al código: PreToolUse detiene las acciones peligrosas, PostToolUse revisa cada edición, Stop impide informar de que todo está listo antes de tiempo y SessionStart devuelve al agente la memoria del proyecto. Empiece con dos o tres hooks para las reglas que el agente ya ha infringido y escriba los mensajes de stderr de forma clara: así el agente corrige sus errores por sí mismo y usted revisa el resultado, no el proceso. Las validaciones, la comprobación de la finalización y los ciclos autónomos los explicamos en la práctica dentro del curso.
Preguntas frecuentes
¿Hace falta reiniciar Claude Code después de cambiar los hooks?
Los cambios en settings.json se aplican con más seguridad en una sesión nueva; la configuración de hooks vigente se puede ver con el comando /hooks.
¿Los hooks tienen que escribirse en bash?
No. Un hook es cualquier comando: un script en Python, Node.js o un binario. Lo único importante es leer el JSON de stdin y devolver el código de salida correcto.
¿El agente ve la salida de un hook que termina bien?
Normalmente no: con código 0 la salida no llega al contexto. Las excepciones son UserPromptSubmit y SessionStart, donde stdout se añade al contexto del agente.
¿Los hooks funcionan también para los subagentes?
Sí. Las llamadas a herramientas de los subagentes pasan por los mismos hooks PreToolUse y PostToolUse, y para su finalización existe el evento específico SubagentStop.
¿Es seguro guardar los hooks en el repositorio?
Sí. El archivo .claude/settings.json y los scripts de .claude/hooks/ son práctica habitual en un equipo; los hooks personales conviene guardarlos en settings.local.json o en ~/.claude/settings.json.
Este tema se explica a fondo y con práctica en el curso «Harness Engineering: инженерия надёжности AI-агентов».
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки