PTENES
INEMA.CLUBPROHooks de Claude Code

Hooks de Claude Code / Ruta 01

Ruta 01 · Fundamentos

Fundamentos de los hooks

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.

5lecciones
~45minutos
principiantenivel

Ruta 01 · Clase 1 · Principiante

Hooks:
o esencial

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

01 Qué es un hook

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:

02 Los momentos (eventos)

Cada hook se conecta a un evento — un momento del trabajo de Claude. Los más importantes para empezar:

  • PreToolUse — antes de que Claude use una herramienta (por ejemplo, antes de ejecutar un comando en el terminal).
  • PostToolUse — después (p. ej.: formatear el archivo en cuanto se edita).
  • UserPromptSubmit — cuando tú envía un mensaje.
  • Stop — cuando Claude termina de responder.
  • SessionStart — cuando la sesión empieza (ideal para inyectar contexto del proyecto).

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.

03 Dónde viven: settings.json

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:

04 Un hook que protege

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:

05 Ideas para empezar

No necesitas muchos. Empieza con un hook que te quite de la cabeza una preocupación recurrente:

  • Formatear al guardar — PostToolUse en Edit/Write ejecuta el formateador. Nunca más código desordenado.
  • Avisar cuando termine — Stop activa una notificación o un sonido.
  • Registrar comandos — PreToolUse en Bash guarda en un log todo lo que se ejecutó.
  • Inyectar las reglas del proyecto — SessionStart imprime tu CLAUDE.md y Claude ya empieza alineado.

El mejor hook es el más tedioso de hacer a mano. Automatiza ese primero.

Profundiza

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

Tu primer hook: registrar comandos

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

settings.json
a fondo

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

01 Tres lugares para los hooks

Un hook puede estar en tres archivos, y todos se aplican al mismo tiempo:

  • Global — ~/.claude/settings.json. Aplica a todos tus proyectos.
  • Proyecto — .claude/settings.json. Va a Git; todo el equipo hereda.
  • Local — .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?

02 Anatomía de un hook

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.

03 El matcher: dónde se activa el hook

Para PreToolUse e PostToolUse, el matcher coincide con el nombre de la herramienta. Tres formas:

  • Nombre exacto — "Bash" toma solo la terminal.
  • Alternancia — "Edit|Write" toma la edición o escritura de archivos.
  • Todo — "*" (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:

04 Qué recibe el hook

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.

Profundiza

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

Mira qué recibe el hook

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

Los eventos,
un por un

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

01 El ciclo de una tarea (línea de tiempo)

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.

02 Antes y después de la herramienta

Cada vez que Claude usa una herramienta — Bash, Edit, Write o cualquiera — dos eventos la rodean:

  • PreToolUse — activa antes de cualquier herramienta ejecutarse. Puede bloquear (exit 2); solo recibe tool_name e tool_input, porque la herramienta todavía no se ejecutó.
  • PostToolUse — activa después que la herramienta terminó correctamente. También recibe el 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?

03 Tú hablas, Claude se detiene

No todos los eventos tienen que ver con herramientas. Algunos marcan el ir y venir de la conversación:

  • UserPromptSubmit — se activa cuando tú envía un mensaje.
  • Stop — se activa cuando Claude termina un turno. También puedes bloquear con exit 2 — no para bloquear una herramienta, sino para obligar a Claude a continuar (p. ej., «todavía falta actualizar el PLAN.md»).
  • SubagentStop — igual que Stop, pero se detiene cuando es un subagente que termina. No bloquea.

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?

04 Sesión y mantenimiento

Los últimos cuatro se encargan del entorno de la sesión, no de una herramienta o mensaje específico:

  • SessionStart — cuando la sesión empieza. Sin matcher; ideal para inyectar contexto (rama, commits, reglas).
  • SessionEnd — cuando la sesión termina normalmente. No bloquea ni modifica nada; sirve para limpiar, medir, archivar algo.
  • PreCompact — antes de que el historial de la conversación sea compactado (resumido para que quepa en el contexto). Es el evento menos documentado; puedes usarlo para guardar algo antes de la compactación, pero no cuentes con detalles precisos.
  • Notification — cuando Claude te necesita (se quedó detenido, pide permiso, un MCP pregunta algo). El único de este grupo con matcher (permission_prompt, idle_prompt, auth_success, elicitation_dialog) — e incluso puedes personalizar el mensaje que se muestra.
Profundiza

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.

05 Quién puede bloquear

Con ocho nombres en mente, es fácil pensar que cualquiera de ellos bloquea algo que sale con exit 2. No es así:

  • PreToolUse — exit 2 bloquea la herramienta antes de que se ejecute.
  • Stop — exit 2 bloquea el final del turno (obliga a Claude a continuar).
  • Todos los demás — PostToolUse, Notification, SessionStart, SessionEnd, SubagentStop, PreCompact, UserPromptSubmit — un exit diferente de 0 solo es error no bloqueante: la acción continúa normalmente (fail-open). En PostToolUse en especial, ni tendría sentido bloquear: la herramienta ya se ejecutó.

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

Un SessionStart que avisa la fecha y el branch

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

Qué hace el
hook recibe

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

01 Todo llega por stdin (JSON)

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.

02 Los campos comunes

Cuatro campos aparecen en todo hook, sin importar el evento:

  • session_id — identifica la sesión actual.
  • cwd — el directorio de trabajo en ese momento.
  • transcript_path — ruta al archivo con el historial de la conversación.
  • hook_event_name — indica qué evento es este (útil si el mismo script atiende más de un evento).

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?

03 tool_input y tool_response

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:

  • tool_input — lo que recibirá la herramienta. Está presente en PreToolUse e PostToolUse.
  • tool_response — el resultado después de que se ejecute la herramienta. Solo existe en 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?

04 Leer con jq

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.

Profundiza

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 tool_name + command con jq

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

Cómo responde el hook
responde

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

01 Dos formas de responder

Un hook se comunica con Claude Code de dos formas que pueden combinarse:

  • Exit code — la señal más simple: un número. Es rápido de escribir y cubre la mayoría de los casos.
  • JSON en stdout — cuando el número no basta. Imprimes un objeto con una decisión explícita, un motivo en texto, y ajustes precisos de comportamiento.

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.

02 Los códigos de salida

Tres franjas de código, tres comportamientos muy diferentes:

  • 0 — si todo sale bien, continúa con normalidad.
  • exit 2 — bloquea, pero solo en 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.

Profundiza

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?

03 JSON de salida

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.
  • reason — el motivo, en texto: lo que antes solo se enviaba por stderr.
  • 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?

04 permissionDecision y contexto extra

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

Un PreToolUse que pregunta en vez de bloquear

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é.