Hooks
Este es el problema de decirle a Claude que haga algo en un archivo CLAUDE.md: es una solicitud, no una garantía. Puedes escribir "siempre formatea después de editar" y Claude normalmente hará caso. Normalmente. Pero en una ejecución larga que no estás observando, "normalmente" no es suficiente. Un hook soluciona eso. Un hook es código determinista que se ejecuta en un punto fijo del ciclo, por lo que puede garantizar un comportamiento en lugar de simplemente esperarlo. Convierte una regla de "Claude normalmente hace caso" en "Claude no puede saltársela".
Esa es toda la propuesta. Ahora veamos cómo funciona realmente.
Los eventos de hook
Claude Code dispara alrededor de 30 eventos de hook a lo largo de una sesión. No necesitas conocerlos todos. Hay un pequeño grupo al que recurrirás una y otra vez, y coinciden con los puntos del ciclo agéntico donde querrías intervenir.
Así es como se ubican en el ciclo. Una sesión comienza, llegan los prompts, se llaman las herramientas y el turno eventualmente termina. Cada uno de esos momentos tiene un hook al que puedes engancharle código.
Los que vale la pena conocer:
- PreToolUse se dispara antes de una llamada a una herramienta. Este es tu primitivo de aplicación. Es el que puede detener algo antes de que suceda.
- PostToolUse se dispara después de una llamada exitosa a una herramienta. Aquí es donde normalmente va el autoformateo o un auto-lint.
- Stop se dispara cuando Claude quiere terminar su turno. Puedes rechazarlo y decir "no, todavía no has terminado" si no se cumple alguna condición. Hay un SubagentStop equivalente para cuando un sub-agente termina.
- PreCompact y PostCompact se disparan antes y después de la compactación.
- InstructionsLoaded se dispara cuando se carga un archivo CLAUDE.md o un archivo de reglas. Útil para auditar qué llegó realmente al contexto.
- SessionStart se dispara al inicio y prepara el entorno. Usa la fuente
startupsi solo lo quieres en inicios nuevos.
Algo que confunde a la gente: para reinyectar contexto después de la compactación, no uses PostCompact. Usa SessionStart con el matcher compact. Ese es el que realmente logra que su salida vuelva a la conversación.
PreToolUse: devolver una decisión como JSON
PreToolUse es donde está el verdadero poder, porque puede bloquear una llamada a una herramienta antes de que se ejecute. La forma de responderle a Claude es imprimiendo JSON y saliendo con código cero. El campo clave es permissionDecision, y toma uno de tres valores:
allow— deja pasar la llamadadeny— detiene la llamadaask— se la devuelve al usuario para que decida
Técnicamente hay un cuarto valor, defer, pero solo se aplica a ejecuciones no interactivas -p donde un proceso que hace la llamada pausa la herramienta y la reanuda más tarde. Rara vez recurrirás a él.
La forma se ve así:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "...",
"updatedInput": {
"command": "..."
}
}
}Fíjate en updatedInput. En lugar de bloquear una llamada, puedes reescribirla. Así es como redactarías un secreto fuera de un comando bash y aun así dejarlo ejecutarse. Una advertencia: updatedInput reemplaza el objeto de entrada completo, así que tienes que devolver también los campos que no estás cambiando, o los perderás.
Códigos de salida, para hooks que no devuelven JSON
No todos los hooks necesitan hablar en JSON. Para hooks más simples, los códigos de salida hacen el trabajo. Hay tres números que importan.
- 0 es éxito. Si la salida estándar es JSON, Claude la analiza. El texto plano se ignora en la mayoría de los eventos, pero en SessionStart, UserPromptSubmit y UserPromptExpansion, el texto plano se agrega al contexto. Eso es exactamente lo que hace funcionar a un hook que preserva el estado.
- 2 es un error de bloqueo. El error estándar se devuelve a Claude como contexto. Este es el código de salida de bloqueo en casi todas partes.
- Cualquier otro no es de bloqueo. El error estándar se registra, y Claude continúa.
El que sorprende a la gente es el código de salida 1. Se siente como un error, pero no bloquea. Claude ejecuta el comando de todos modos. Así que si tu intención era detener algo, usa exit 2, no 1.
Un par de matices más. Exit 2 puede incluso bloquear Stop, que es cómo le dices a Claude que no ha terminado. Pero PostToolUse se dispara después de que la herramienta ya se ejecutó, así que bloquear ahí llega demasiado tarde para detener la llamada, aunque todavía puede devolverle texto a Claude. Y algunos eventos ignoran el bloqueo por completo, como Notification y SessionStart. SessionStart te muestra tu error estándar solo a ti, Notification lo ignora, y ambos continúan de todos modos.
Una protección real: redactar en lugar de bloquear
Juntemos todo con algo práctico. Digamos que quieres una protección de PreToolUse en la herramienta Bash. El matcher elige la herramienta a observar, y una cláusula if opcional puede acotarlo a un comando específico.
El movimiento obvio es devolver deny y detener una llamada peligrosa. Eso está bien. Pero el movimiento menos conocido y más interesante es devolver updatedInput para reescribir la llamada. Así es como eliminas un secreto de un comando y aun así lo dejas ejecutarse, en lugar de simplemente rechazarlo.
Así es como se ve esto en la práctica. Se le pide a Claude que ejecute un comando que incluye un secreto que parece real. El hook lo intercepta, detecta el patrón sk_live_, y lo reemplaza por un marcador de posición antes de que el comando llegue a ejecutarse.
El comando se ejecutó de todos modos. El trabajo se completó. Pero el secreto nunca pasó. Esa es la diferencia entre bloquear y redactar, y es el tipo de cosa que un hook puede hacer cumplir cada vez.
Preservar el estado a través de una compactación
Un patrón más que vale la pena configurar. Cuando Claude compacta una conversación larga, descarta mucho detalle. Un hook de SessionStart con el matcher compact se ejecuta justo después de la compactación. Haz que imprima un breve resumen de los archivos en los que has estado trabajando. Ese resumen vuelve al contexto, así que Claude retoma donde lo dejó en lugar de empezar desde cero.
Para cerrar
Los hooks convierten una regla que Claude normalmente sigue en una que siempre sigue. Ve más allá del autoformateo: protege las herramientas con PreToolUse, controla el turno con Stop, y preserva el estado a través de una compactación. La configuración requiere un poco de esfuerzo al principio, pero se recupera la primera vez que detecta algo en una ejecución que ni siquiera estabas observando.