TL;DR Un hook
"type": "prompt"manda tu condición y el JSON del evento a un modelo rápido, que responde{"ok": true|false, "reason": "..."}. Sirve para reglas que un script no sabe comprobar. Tres cosas que decidir antes de escribirlo: el evaluador lee la conversación y no tu repositorio, enPreToolUsenecesitascontinueOnBlocko el turno muere al primer bloqueo, y si no le dices cuándo devolverimpossiblete puede dejar en bucle.
Un hook de tipo command responde con un exit code. Eso cubre todo lo que se puede comprobar con un script: si el fichero existe, si el linter pasa, si el comando casa con un patrón.
Lo que no cubre son las reglas que son un juicio. "No pares si has dejado tests rojos". "No ejecutes ese comando si va a borrar datos de verdad". Escribir eso en bash significa una cascada de grep que falla con el primer caso que no previste. Hay otro tipo de hook para esto, y puede que ya lo hayas usado sin montarlo: /goal es exactamente un hook prompt sobre Stop, empaquetado para una sesión y un solo caso. Montarlo tú mismo te da el primitivo entero: cualquiera de los trece eventos que lo admiten, permanente en tu settings.json, y con cuatro campos que /goal no expone.
Cómo funciona
En vez de lanzar un proceso, Claude Code manda tu prompt a un modelo rápido (Haiku por defecto) junto con el JSON del evento, que entra donde pongas $ARGUMENTS. El modelo contesta con un JSON fijo: ok, reason y opcionalmente impossible. Es una sola llamada, sin herramientas.
Eso define el límite: el evaluador lee la transcripción de la conversación, no tu proyecto. Si la condición no se puede comprobar con lo que Claude ya ha escrito en el chat, no la puede juzgar. Y si la conversación es larga, la recorta por presupuesto y le añade una instrucción para que responda insufficient evidence in transcript cuando lo que necesita se haya quedado fuera (verificado en el binario de la 2.1.278). Condiciones cortas y medibles, no interpretaciones.
Cómo montarlo
1. El bloque mínimo
En ~/.claude/settings.json o en el .claude/settings.json del proyecto:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Contexto: $ARGUMENTS\n\n¿Ha ejecutado la suite de tests y ha terminado en verde? Responde {\"ok\": true, \"reason\": \"...\"} si la transcripción lo demuestra, o {\"ok\": false, \"reason\": \"qué falta\"} si no.",
"timeout": 30
}
]
}
]
}
}
En Stop, un ok: false devuelve tu reason a Claude como su siguiente instrucción y el turno continúa. Es el mismo ciclo de /goal, pero en todas tus sesiones y sin tener que declararlo cada vez.
2. Dale una salida o te quedas en bucle
impossible no es un campo de configuración, es parte de la respuesta del modelo. El evaluador puede devolverlo por su cuenta, pero la forma de que salga cuando te hace falta es pedirlo en el prompt:
Si la condición no se puede cumplir nunca (el fichero no existe, la tarea no aplica),
responde {"ok": false, "reason": "...", "impossible": true}.
Con impossible: true, Claude Code deja terminar el turno en lugar de reinyectar la razón. Sin esa frase, una condición imposible se reevalúa turno tras turno.
3. En PreToolUse, continueOnBlock no es opcional
El mismo hook sobre PreToolUse es un guardián semántico: juzga el comando antes de ejecutarlo. Pero ahí el valor por defecto cambia el resultado. Con continueOnBlock en false (el predeterminado), un ok: false termina el turno y la razón sale como aviso. Con true, la razón vuelve a Claude como error de la tool y sigue trabajando, que es lo que casi siempre quieres:
{
"type": "prompt",
"prompt": "Comando propuesto: $ARGUMENTS\n\n¿Borra o sobrescribe datos que no se pueden recuperar? {\"ok\": false, \"reason\": \"...\"} si sí.",
"continueOnBlock": true
}
Para filtrar antes de llamar al modelo, la misma entrada admite if con una regla de permisos: hooks condicionales.
Lo que la validación caza y lo que no
Un hook prompt mal escrito no da error, desaparece. Si lo distribuyes en un plugin, claude plugin validate sí lo dice, con el literal:
❯ hooks: hooks.Stop.0.hooks.0: Invalid prompt hook (prompt: Invalid input);
entry ignored at runtime
Lo que no detecta: poner un hook prompt en un evento que no lo admite. Con uno colocado en SessionStart la validación pasa limpia y sin avisos. Los trece eventos que aceptan prompt son PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, Stop, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle, UserPromptSubmit y UserPromptExpansion. En SessionStart, PreCompact o Notification sigues necesitando un hook de tipo command.
prompt o agent
Si la condición necesita abrir ficheros o ejecutar la suite de tests, el tipo no es prompt sino agent: mismo formato, pero levanta un subagente con herramientas y hasta 50 turnos. Es experimental y su timeout por defecto sube a 60 segundos. La regla práctica: prompt juzga lo que ya está en la conversación, agent va a comprobarlo.
Referencia
| Campo | Dónde | Valor |
|---|---|---|
prompt |
configuración | Obligatorio. $ARGUMENTS recibe el JSON del evento; si no lo pones, se añade al final |
model |
configuración | Opcional. Por defecto, el modelo rápido (Haiku) |
timeout |
configuración | Opcional. 30 segundos |
continueOnBlock |
configuración | Opcional. false por defecto: el turno termina al bloquear |
ok |
respuesta | true permite, false bloquea |
reason |
respuesta | Obligatorio cuando ok es false |
impossible |
respuesta | Con ok: false, en Stop y SubagentStop deja terminar el turno |
Documentación oficial: Hooks reference