Hooks de Claude Code / Ruta 01
Ruta 01 · Fundamentos
Qué es un hook, los eventos que activan cada uno y dónde vive todo en el settings.json, el JSON que llega y cómo decide tu hook qué responder.
Lecciones de esta ruta
Qué es un hook, los eventos, dónde configurarlo, cómo bloquear lo peligroso e ideas para empezar.
Los tres lugares de los hooks, la anatomía evento→matcher→comando, los matchers y lo que recibe el hook.
El ciclo completo de una tarea, los eventos que existen y quién puede bloquear realmente entre ellos.
El JSON que llega por stdin: los campos comunes, tool_input/tool_response y cómo leerlo con jq.
Exit code o JSON: decision, reason y permissionDecision, que solicita confirmación en lugar de bloquear.
Ruta 01 · Clase 1 · Principiante
Un hook es una instrucción automática: «cada vez que ocurra esto, ejecuta aquello». Así es como Claude Code sigue tus reglas por sí solo, sin depender de que las recuerdes.
↓ desplázate para estudiar
Un hook es una orden automático que le das a Claude Code: «cada vez que ocurra esto, ejecuta aquello". O esto es un momento (un evento); aquello es un comando tuyo: un script, un formateador, un aviso. Claude lo ejecuta automáticamente, sin que tengas que volver a pedirlo.
Sin un hook, repites las mismas instrucciones ("formatea el código", "no ejecutes eso") en cada sesión y esperas que se sigan. Con un hook, la regla se convierte en parte del entorno: automática y con el mismo comportamiento cada vez.
Pruébate
En la práctica, un hook es:
Cada hook se conecta a un evento — un momento del trabajo de Claude. Los más importantes para empezar:
Cada evento entrega a tu comando los detalles de lo que está pasando (qué herramienta, qué archivo) en JSON por la entrada estándar. Tu comando lo lee y decide qué hacer.
Los hooks viven en settings.json — en el proyecto (.claude/settings.json) o global (~/.claude/settings.json). Tú dices el evento, un matcher (en qué herramienta se aplica) y el comando:
// .claude/settings.json — formatea todos los archivos editados { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "prettier --write $file" } ] } ] } }
Lee de adentro hacia afuera: en el evento PostToolUse, cuando la herramienta es Edit o Write, ejecute el comando. Eso es todo: tres capas: evento → matcher → comando.
Pruébate
O matcher "Edit|Write" sirve para:
En el PreToolUse, tu comando puede bloquear la acción antes de que se ejecute. Si el comando sale con código 2, Claude no ejecuta eso, y el mensaje que imprimes se convierte en el motivo mostrado a esa persona. Así es como bloqueas lo peligroso (un rm -rf, un push a main) antes de que ocurra.
# bloquea rm -rf; recibe el comando por stdin (JSON)
grep -q "rm -rf" && { echo "bloqueado: comando destrutivo" >&2; exit 2; }
exit 0
Exit 0 = puedes continuar. Exit 2 = bloquea y devuelve el motivo. Simple y potente: conviertes «por favor, no lo hagas» en una bloqueo real.
Pruébate
Para el hook PreToolUse bloquear un comando, debe salir con código:
No necesitas muchos. Empieza con un hook que te quite de la cabeza una preocupación recurrente:
El mejor hook es el más tedioso de hacer a mano. Automatiza ese primero.
Además del exit code, los hooks pueden devolver JSON por la salida (una decisión de aprobar/bloquear con un motivo), usar matchers más precisos, y hay otros eventos: SubagentStop, Notification, PreCompact, SessionEnd. Continúa en las próximas clases.
Practica ahora 0/4 hecho
Haz que Claude registre cada comando en un archivo y mira cómo funciona. Unos 5 minutos, y es seguro: el hook solo observa (sale con exit 0), no bloquea nada.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "cat >> .claude/cmd.log" }
]
}
]
}
}✓ Listo: tu primer hook se ejecutó por sí solo. Es exactamente esto — cuando esto → ejecuta aquello. En las próximas clases será más hábil: formateará, avisará y bloqueará lo peligroso.
Ruta 01 · Clase 2 · Principiante
Dónde viven los hooks, cómo se compone cada uno y qué le entrega Claude a tu comando al ejecutarlo.
↓ desplázate para estudiar
Un hook puede estar en tres archivos, y todos se aplican al mismo tiempo:
~/.claude/settings.json. Aplica a todos tus proyectos..claude/settings.json. Va a Git; todo el equipo hereda..claude/settings.local.json. Está en .gitignore; é solo tuyo.Cuando hay un conflicto, el gana el más específico: la configuración local prevalece sobre la del proyecto, que prevalece sobre la global. Regla práctica: lo que es del equipo va en .claude/settings.json; lo que es una preferencia tuya va en el .local.json.
Pruébate
¿Qué archivo es personal y NO va a Git?
Todo hook tiene la misma forma de muñecas rusas: el evento guarda una lista; cada elemento tiene un matcher y una lista de hooks; cada hook es un comando.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./guarda.sh" }
]
}
]
}
}
Puede haber varios hooks en el mismo matcher: se ejecutan en orden. Y varios matchers en el mismo evento. Empieza con uno; la estructura puede crecer.
Para PreToolUse e PostToolUse, el matcher coincide con el nombre de la herramienta. Tres formas:
"Bash" toma solo la terminal."Edit|Write" toma la edición o escritura de archivos."*" (o vacío) captura cualquier herramienta.Los eventos que no son de herramienta (como SessionStart o Stop) no usan un matcher de herramienta: simplemente se activan en ese momento.
Pruébate
Para que un hook detecte tanto Edit como Write, el matcher es:
Al ejecutarse, Claude le entrega a tu comando un JSON por la entrada estándar (stdin) con el contexto: qué evento ocurrió, qué herramienta se usó y cuál fue su entrada.
# lee el comando que Bash iba a ejecutar cmd=$(jq -r '.tool_input.command') # lee el archivo que se editó file=$(jq -r '.tool_input.file_path')
También está la variable $CLAUDE_PROJECT_DIR apuntando a la raíz del proyecto — útil para encontrar tus scripts. Con stdin + jq, tu hook sabe exactamente lo que está a punto de suceder.
El JSON trae más campos (session_id, cwd, hook_event_name…). Rara vez necesitas todos: tool_name e tool_input resuelven la mayoría de los hooks. La Lección 4 de esta ruta desglosa ese JSON campo por campo.
Practica ahora 0/4 hecho
Registra el JSON que Claude entrega al hook y lee con tus propios ojos el tool_input. ~5 min, seguro (solo observa).
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "cat >> .claude/edits.log" }
]
}
]
}
}✓ Viste el payload real. El matcher Edit|Write y stdin dejan de ser una abstracción: este es el JSON que lee el hook.
Ruta 01 · Clase 3 · Principiante
Desde el inicio de la sesión hasta el final de una tarea: los momentos en que un hook puede engancharse y cuáles de ellos realmente pueden bloquear algo.
↓ desplázate para estudiar
Una sesión de Claude Code tiene un ciclo de vida, y cada vuelta abre una ventana para un hook. En orden: la sesión empieza, tú envía un mensaje, Claude eventualmente usa herramientas (una por una, antes y después de cada una), termina de responder, y — tarde o temprano — la sesión termina.
Pensar en los hooks como puntos en una línea de tiempo ayuda más que memorizar nombres sueltos: cada evento es solo «esto acaba de pasar» — y el resto de la lección consiste en ver, uno por uno, qué permite hacer cada punto.
Cada vez que Claude usa una herramienta — Bash, Edit, Write o cualquiera — dos eventos la rodean:
tool_name e tool_input, porque la herramienta todavía no se ejecutó.tool_response (el resultado). Un exit 2 aquí no bloquea — la herramienta ya se ejecutó.Es la misma herramienta, pero en dos momentos con poderes diferentes: Pre decide si se ejecuta; Post reacciona a lo que se ejecutó.
Pruébate
¿Por qué solo PreToolUse puede impedir que se ejecute una herramienta?
No todos los eventos tienen que ver con herramientas. Algunos marcan el ir y venir de la conversación:
Observa la asimetría: Stop es una de las dos puertas que realmente bloquean (la otra es PreToolUse); UserPromptSubmit e SubagentStop no bloquean: observan o inyectan contexto.
Pruébate
¿Qué evento se activa cuando TÚ envías un mensaje, antes de que Claude lo procese?
Los últimos cuatro se encargan del entorno de la sesión, no de una herramienta o mensaje específico:
permission_prompt, idle_prompt, auth_success, elicitation_dialog) — e incluso puedes personalizar el mensaje que se muestra.SubagentStop e PreCompact son los dos eventos con documentación oficial más incompleta; funcionan, pero espera menos previsibilidad detallada que en PreToolUse o Stop. Pruébalos siempre antes de confiarles algo crítico.
Con ocho nombres en mente, es fácil pensar que cualquiera de ellos bloquea algo que sale con exit 2. No es así:
Guarda esta regla: solo Pre y Stop bloquean de verdad. Esto evita la trampa de escribir un bloqueo en un evento que nunca impedirá nada.
Pruébate
Un hook PostToolUse sale con exit 2. ¿Qué ocurre?
Practica ahora 0/4 hecho
Haz que la sesión imprima dónde y cuándo comenzó: lo primero que se ejecuta, antes de cualquier herramienta. ~5 min, seguro (solo observa).
{
"hooks": {
"SessionStart": [
{
"hooks": [
{ "type": "command", "command": "{ date; echo branch: $(git branch --show-current); } >> .claude/session.log" }
]
}
]
}
}✓ SessionStart es la primera ventana del ciclo — sin matcher y sin depender de ninguna herramienta. Es el lugar indicado para cualquier contexto que Claude debería conocer desde el primer segundo.
Ruta 01 · Clase 4 · Principiante
Todo hook empieza igual: un JSON entra por la entrada estándar. Aprende a leer ese payload — los campos que siempre están y los que cambian según el evento.
↓ desplázate para estudiar
No importa el evento: cuando se activa un hook, Claude Code crea un objeto JSON con el contexto de ese momento y entrega mediante la entrada estándar (stdin) de tu comando. Tu script lee este texto — con cat, jq, un json.load(sys.stdin) en Python — y decide qué hacer.
Una consecuencia práctica: los hooks no pueden ser interactivos. Reciben todo de una vez, al principio, y responden de una vez, al final (mediante un código de salida o un JSON impreso). No puedes «preguntarles» nada a mitad del proceso.
Cuatro campos aparecen en todo hook, sin importar el evento:
cwd — el directorio de trabajo en ese momento.Piensa en ellos como el sobre: siempre aparece y te orienta incluso antes de mirar el resto del contenido.
Pruébate
¿Cuál de estos campos aparece en CUALQUIER hook, sea cual sea el evento?
Aparte del sobre común, el resto del JSON cambia según el evento. En los eventos de herramientas, el par más importante es:
PreToolUse e PostToolUse.PostToolUse — en PreToolUse la herramienta todavía no se ejecutó.Otros eventos traen sus propios campos: Notification viene con notification_type e notification_message; SessionStart no aporta ningún dato de la herramienta (no hay nada que aportar: aún no se ha ejecutado nada). Siempre revisa el hook_event_name antes de asumir que un campo estará ahí.
Pruébate
¿Por qué el campo tool_response no aparece en el JSON de un PreToolUse?
En la práctica, tres comandos resuelven casi todo:
# nombre de la herramienta jq -r '.tool_name' # el comando que Bash iba a ejecutar jq -r '.tool_input.command' # el archivo que se editó jq -r '.tool_input.file_path'
Para explorar un payload completo mientras aprendes, jq . (sin filtro) imprime todo con formato y de forma legible: sirve para depurar un hook nuevo antes de escribir el filtro correcto.
Sin jq instalado, puedes leer el mismo JSON en Python: import sys, json; data = json.load(sys.stdin), y luego data.get("tool_name"). Más verboso, pero funciona en cualquier máquina con Python.
Practica ahora 0/4 hecho
Registra en detalle qué herramienta se ejecutó y qué comando recibió — directamente del JSON de stdin. ~5 min, seguro (solo observa).
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_name + \" \" + .tool_input.command' >> .claude/tool.log" }
]
}
]
}
}✓ Pasaste del payload sin procesar a un registro legible con un filtro de una línea. Esa es la distancia real entre «llegó un JSON» y «mi hook decide algo con él».
Ruta 01 · Clase 5 · Principiante
Exit code es la señal sencilla. Pero un hook también puede devolver un JSON y explicar la decisión que tomó con un motivo.
↓ desplázate para estudiar
Un hook se comunica con Claude Code de dos formas que pueden combinarse:
Empieza siempre por el código de salida: es el camino rápido de las clases anteriores. Migra a JSON cuando necesites más matices: pedir confirmación en lugar de bloquear, o explicar por qué de una forma más completa que una línea en stderr.
Tres franjas de código, tres comportamientos muy diferentes:
0 — si todo sale bien, continúa con normalidad.PreToolUse e Stop. Lo que imprimes en el stderr se convierte en el motivo.1, 3 o más — error no bloqueante: la acción continúa (fail-open). Tu hook falló, pero Claude no se bloquea por eso.Este último punto es deliberado: un hook mal formado no debería poder bloquear todo Claude Code. Prefiere fallar de forma abierta — si tu script tiene un error, el peor escenario es que el hook no haga nada, no que Claude se quede detenido.
Este principio se llama fail-open: en tu script, siempre ten una ruta de salida garantizada (un trap en el shell, un try/except en Python) que devuelve exit 0 si ocurre algo inesperado. Un hook de seguridad que se bloquea solo es peor que no tener ningún hook.
Pruébate
Un hook Notification sale con exit 1 por un error en el script. ¿Qué sucede?
En lugar de solo un exit code, el hook puede imprimir un JSON en stdout con una decisión más expresiva:
{
"decision": "approve|block",
"reason": "string",
"suppressOutput": false,
"continue": true
}
Cada campo cumple una función:
decision — "approve" o "block", la versión explícita del código de salida.suppressOutput — oculta la salida del hook del transcript cuando no le interesa a nadie.continue — si false, para toda la sesión, no solo para la acción actual. Úsalo con cuidado.Pruébate
¿Qué campo del JSON de salida explica, en texto, el motivo de una decisión?
Para entregar contexto de verdad, el hook devuelve
{
"hookSpecificOutput": {
"permissionDecision": "allow|deny|ask"
},
"reason": "confirmar antes de rodar em produção"
}
Tres posturas, no dos: allow (permite), deny (barra — equivalente a exit 2) y ask (pausa y pregunta a ti, en lugar de decidir por su cuenta). Esta es la tercera opción que el código de salida por sí solo no ofrece: no todo es blanco o negro, y a veces lo correcto es solo avisar.
Algunos eventos también aceptan un additionalContext dentro del JSON — texto adicional que entra en el contexto de Claude, además de la decisión en sí. Es como inyectar una nota al pie en lo que está procesando.
Practica ahora 0/4 hecho · capstone
Cierra la ruta haciendo que un hook responda con permissionDecision: "ask" y un motivo — sin borrar nada. ~5 min, seguro: solo pide confirmación.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command",
"command": "grep -q PEDECONFIRMACAO && printf '{\"hookSpecificOutput\":{\"permissionDecision\":\"ask\"},\"reason\":\"confirmar antes de rodar\"}'; exit 0" }
]
}
]
}
}✓ Fin de la ruta: tu hook no solo decide (exit code), también explica la decisión en JSON, con permissionDecision y un motivo. Es el mismo cuando esto → ejecuta aquello de la Aula 1, solo que ahora Claude entiende exactamente por qué.