\
CONTEXT/academy
Lección 02 · ≈ 18 min

Anatomía de un hook

Eventos, tipos de handler, la estructura en settings.json, el input por stdin, y la decisión: exit codes o JSON.

Anatomía de un hook

Antes de construir hooks útiles, hay que entender sus piezas: cuándo se dispara (el evento), para qué acción (el matcher), qué ejecuta (el handler, casi siempre un command), y cómo decide qué pasa con la acción (la decisión: exit codes o JSON). Con esto, el resto del curso son variaciones.

1. Los eventos: cuándo se dispara

Un hook se engancha a un punto del ciclo de vida. Los que más usarás:

EventoSe dispara…
PreToolUseantes de ejecutar una tool (puede bloquearla)
PostToolUsedespués de que una tool tenga éxito
UserPromptSubmitcuando envías un prompt, antes de procesarlo
SessionStartal empezar, reanudar, tras /clear o tras compactar (lo distingues por el source)
Stop / SubagentStopcuando Claude (o un subagente) termina de responder

La regla mental: para impedir algo, PreToolUse (corre antes y puede vetar). Para reaccionar a algo ya hecho (formatear, loguear), PostToolUse.

Esos son los del día a día, pero el catálogo es mucho más amplio: hay eventos para subagentes (SubagentStart), compactación (PreCompact/PostCompact), fin de sesión (SessionEnd), carga de CLAUDE.md (InstructionsLoaded), cambios de config (ConfigChange), worktrees, notificaciones (MessageDisplay)… La referencia tiene la lista entera; para la suite de este curso te bastan los cinco de arriba.

2. La estructura en settings.json

Los hooks viven en settings.json con tres niveles: evento → matcher → handler. El matcher filtra qué tools disparan el hook ("Bash", "Edit|Write", o vacío para todas).

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
        ]
      }
    ]
  }
}

Como cualquier cosa en .claude/settings.json, esto se versiona y lo hereda el equipo (lo viste en el curso de permisos).

El type: "command" no es el único. Un hook puede ser también http (llama a un endpoint), mcp_tool, o —basados en modelo— prompt y agent. Pero esos dos últimos vuelven a depender del juicio del modelo: ya no son deterministas. Todo este curso usa command, que es lo que ejecuta código y siempre hace lo mismo. Es el muro.

Cada handler command admite además unos campos opcionales que conviene conocer:

CampoPara qué
timeoutsegundos máximos antes de abortar el hook
ifcondición declarativa estilo permiso ("Bash(rm *)") — el hook solo corre si el comando casa, sin tener que filtrar tú en el script
argsargumentos en exec-form: lanza el binario directo, sin shell, así los ${CLAUDE_PROJECT_DIR} y demás no necesitan comillas
statusMessagetexto que se muestra en el spinner mientras corre

3. El input: JSON por stdin

Cuando se dispara, Claude Code le pasa al command un JSON por stdin con el contexto de la acción. Los campos clave:

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf /tmp/build" }
}

Según la tool, tool_input trae unos campos u otros: Bashcommand; Writefile_path, content; Editfile_path, old_string, new_string. Por eso los hooks suelen usar jq para extraer lo que les interesa:

COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)

4. La salida: exit 0 vs exit 2

Así decide un hook qué pasa con la acción:

Exit codeEfecto
0Éxito. La acción continúa. (Si imprime JSON en stdout, se usa para control estructurado.)
2Bloqueo. La acción se cancela y el stderr se le devuelve a Claude como mensaje de error.
otroError no bloqueante: se loguea, la ejecución continúa.

El patrón más común de todo el curso —bloquear algo— es exactamente esto: en un hook PreToolUse, si detectas lo que no quieres, escribes el motivo a stderr y haces exit 2.

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
if echo "$COMMAND" | grep -q 'rm -rf'; then
  echo "Bloqueado: rm -rf no está permitido." >&2
  exit 2          # cancela la acción y avisa a Claude
fi
exit 0            # todo OK, sigue

Los scripts suelen ir en .claude/hooks/ y se referencian con "$CLAUDE_PROJECT_DIR"/.claude/hooks/script.sh. Recuerda chmod +x.

5. El control fino: salida en JSON

exit 2 es la vía rápida y la que usamos en todo el curso. Pero cuando un hook necesita más matiz que "bloquea / deja pasar", en vez de un código de salida puede imprimir JSON en stdout (con exit 0). Lo más útil:

# En un PreToolUse: denegar con motivo, sin depender del exit code
jq -n '{
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: "Comando destructivo bloqueado por el hook"
  }
}'
  • permissionDecision (en PreToolUse) toma allow / deny / ask / defer: no solo bloqueas, también puedes pre-aprobar o forzar la pregunta.
  • additionalContext te deja inyectar texto en el contexto de Claude (envuelto en un recordatorio del sistema, no como mensaje de chat). Ideal para guiar en vez de solo cortar — p.ej. desde un PostToolUse: "este fichero es generado; edita src/schema.ts y corre bun generate".
  • Campos universales como continue: false (paran el turno entero) o systemMessage (aviso visible al usuario) aplican a cualquier hook.

Cuándo cada uno: exit 2 para el bloqueo simple (el 90% del curso). JSON cuando quieras decidir entre allow/ask/deny, devolver contexto, o afinar el comportamiento. Los dos conviven; empieza por el exit code y sube a JSON cuando te quede corto.

Qué llevas hasta aquí

  • Un hook = evento (cuándo) + matcher (para qué tool) + handler (qué, normalmente type: command) + decisión (exit code o JSON).
  • PreToolUse para impedir; PostToolUse para reaccionar; y un catálogo amplio de eventos más allá de esos.
  • El command recibe JSON por stdin (usa jq) y decide con exit 2 + stderr (bloqueo simple) o con JSON en stdout (permissionDecision, additionalContext) para control fino.

Con esto montado en la cabeza, la siguiente lección construye el primer hook real: formateo automático.

Fuente oficial: Hooks reference

¿Quieres llevar la cuenta de las lecciones que has terminado?

Entrar para guardar progreso