PTENES
INEMA.CLUBPROHooks de Claude Code

Hooks de Claude Code / Ruta 03

Ruta 03 · Automatización

Reglas que se ejecutan
por sí solas

Cuatro hooks listos para copiar y pegar: formatear al guardar, avisar cuando termine, registrar cada comando e inyectar contexto en cuanto se abre la sesión.

4lecciones
~36minutos
intermedionivel

Ruta 03 · Lección 1 · Intermedio

Formatear
al guardar

Después de que Claude edita un archivo, un hook ejecuta el formateador en él, sin que tengas que recordarlo y sin excepciones.

↓ desplázate para estudiar

01 PostToolUse en Edit|Write

El hook vive en el evento PostToolUse: lo activa después que Claude termina de editar o escribir un archivo: la acción ya ocurrió, es hora de ordenar todo.

O matcher filtra en qué herramientas se ejecuta el hook: "Edit|Write" toma las dos: edición de un archivo existente o escritura de uno nuevo.

{ "hooks": { "PostToolUse": [ {
  "matcher": "Edit|Write",
  "hooks": [ { "type": "command",
    "command": "f=$(jq -r .tool_input.file_path); prettier --write \"$f\"" } ]
} ] } }

Lee de adentro hacia afuera: en el evento PostToolUse, cuando la herramienta sea Edit o Write, ejecuta el comando. Sencillo — y te lo quitas de la cabeza para siempre.

Pruébate

¿Qué evento se ejecuta DESPUÉS de que una herramienta termina de ejecutarse?

02 Capturar el archivo

El hook no sabe de antemano qué archivo se modificó: lo lee del JSON de stdin, campo tool_input.file_path. Con jq, esto se convierte en una variable de shell:

# lee el archivo que se acaba de editar
file=$(jq -r '.tool_input.file_path')

Es este $file que irá al formateador en la siguiente sección. Sin él, el hook no sabría sobre qué archivo actuar.

03 Ejecutar el formateador

Con la ruta en mano, elige el formateador según la extensión del archivo. Un case resuelve para cualquier stack:

case "$file" in
  *.js|*.ts|*.jsx|*.tsx) prettier --write "$file" ;;
  *.py) black -q "$file" ;;
  *.go) gofmt -w "$file" ;;
esac

Cámbialos por los formateadores de tu proyecto. El punto no es qué herramienta sea, sino que se ejecute siempre, en todo archivo, sin excepción humana.

Pruébate

Para formatear un archivo .py, qué línea del case ¿entra en acción?

04 Silencioso e idempotente

Un hook de formato no debe bloquear nada: siempre exit 0. Y cuidado con una trampa — en el PostToolUse, un exit 2 no deshace la edición (la herramienta ya se ejecutó); solo informa de un error.

Para no llenar el contexto cuando todo salió bien, usa suppressOutput:

{ "suppressOutput": true }

Y el formateador debe ser idempotente: ejecutar dos veces sobre el mismo archivo no puede romper nada: la misma entrada, el mismo resultado, siempre.

Profundiza

Solo PreToolUse e Stop pueden bloquear algo con exit 2. En el PostToolUse, ese mismo código se trata como un error no bloqueante: la acción ya ocurrió, no hay forma de revertirla.

Pruébate

Si un hook PostToolUse sale con exit 2, ¿qué ocurre?

Practica ahora 0/4 hecho

Prettier en el archivo recién editado

Haz que Claude formatee por sí solo todos los archivos que edite. ~5 min, seguro: solo formatea, no bloquea nada.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "f=$(jq -r '.tool_input.file_path'); command -v prettier >/dev/null && prettier --write \"$f\" 2>/dev/null; exit 0" }
        ]
      }
    ]
  }
}

✓ Cierre: el código siempre queda limpio — el formateador pasó a ser parte del flujo, no una tarea manual que puedes olvidar.

Ruta 03 · Lección 2 · Intermedio

Avisar cuando
terminar

Dos eventos avisan en el momento indicado: Stop cuando Claude termina de responder, Notification cuando te necesita.

↓ desplázate para estudiar

01 Stop y Notification

Dos eventos avisan de cosas diferentes. Stop se activa cuando Claude termina de responder — el turno terminó. Ya Notification se activa cuando él te necesita: estuvo inactivo 60s o más, pidió un permiso o un MCP está esperando una respuesta.

La diferencia importa: Stop es «terminé»; Notification es «te estoy esperando». Cada uno requiere un tipo de aviso diferente.

Pruébate

¿Qué evento se activa cuando Claude necesita tu permiso (o queda detenido durante 60s o más)?

02 Activar notify-send / un sonido

Agrega una notificación de escritorio al evento que te interesa. En Linux, notify-send resuelve:

# Stop → notificación de escritorio (linux)
notify-send "Claude Code" "terminou de responder"

Un sonido corto (paplay, afplay) también funciona y es más discreto que una ventana emergente. Lo importante: tú deja de vigilar la terminal.

03 Solo cuando importa

El evento Notification se activa por varios motivos — notification_type dice cuál. Si notificas en todos, se convierte en spam. Filtra lo que realmente necesita tu atención:

type=$(jq -r '.notification_type')
[ "$type" = "permission_prompt" ] || exit 0
notify-send "Claude Code" "precisa de permissão" --urgency=critical

Así, solo la solicitud de permiso activa la alerta crítica; el resto (idle, elicitation) lo decides tú, si quieres o no, sin tocar todo a cada rato.

Pruébate

Para notificar solo cuando se solicite permiso (e ignorar el resto), filtra por:

04 Variaciones por SO

El comando de notificación cambia según el sistema. Un case encima de uname cubre los tres:

case "$(uname)" in
  Linux)  notify-send "Claude Code" "terminou" ;;
  Darwin) osascript -e 'display notification "terminou" with title "Claude Code"' ;;
  *)      powershell.exe -Command "New-BurntToastNotification -Text 'Claude Code','terminou'" ;;
esac

En Windows (vía WSL), llamar al powershell.exe dentro de Linux suele ser el camino más directo.

Profundiza

¿Prefieres sonido a una ventana emergente? Cambia el notify-send/osascript por paplay algum.wav (Linux) o afplay algum.wav (macOS) — más discreto, funciona igual como señal de "terminó".

Practica ahora 0/4 hecho

Notificación de escritorio en el evento Stop

Haz que Claude te avise con una ventana emergente cada vez que termine de responder. ~5 min, seguro (solo observa, no bloquea).

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "command -v notify-send >/dev/null && notify-send 'Claude Code' 'terminou de responder'; exit 0" }
        ]
      }
    ]
  }
}

✓ Cierre: ya no tienes que vigilar la terminal — el aviso llega hasta ti.

Ruta 03 · Lección 3 · Intermedio

Registrar
los comandos

PreToolUse en Bash registra cada comando con la hora: un diario de todo lo que ejecutó la sesión, sin bloquear nada.

↓ desplázate para estudiar

01 PreToolUse en Bash

El evento PreToolUse se activa antes de cualquier herramienta ejecutarse. Con matcher "Bash", toma solo los comandos de terminal, antes de que se ejecuten de verdad.

Aquí no vamos a bloquear nada: el hook solo observa y deja que continúe. Es un registro, no un bloqueo.

Pruébate

Un hook PreToolUse con matcher "Bash" ¿se activa cuándo?

02 Añadir a un log con timestamp

Cada línea del log lleva la hora en que se iba a ejecutar el comando. Construye la marca de tiempo con date:

# .claude/hooks/log-cmd.sh (PreToolUse, matcher "Bash")
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)

Esto por sí solo todavía no hace nada — falta capturar el comando en sí, que viene en la siguiente sección.

03 Extraer el command con jq

El texto del comando viene en el JSON de stdin, en el campo tool_input.command. Combínalo con la marca de tiempo y adjúntalo a un archivo:

cmd=$(jq -r '.tool_input.command')
printf '%s\t%s\n' "$ts" "$cmd" >> "$CLAUDE_PROJECT_DIR/.claude/cmd.log"
exit 0

$CLAUDE_PROJECT_DIR garantiza que el registro siempre vaya al mismo lugar, sin importar desde dónde se haya ejecutado el comando. Y exit 0 — el hook nunca bloquea, solo registra.

Pruébate

¿De dónde viene el texto del comando que Bash va a ejecutar?

04 Auditoría simple de lo que se ejecutó

O cmd.log se convierte en un registro: cada comando, con hora y en orden. Útil para revisar lo que hizo una sesión, investigar un comando extraño o reconstruir una cronología después.

Una precaución: los hooks se ejecutan con permisos totales de tu usuario. Si un comando tiene un token o una contraseña incorporados, quedarán en el registro: no dejes que ese archivo salga del .gitignore ni circular por ahí.

Profundiza

El registro es solo para que lo consultes después; el hook en sí nunca debe bloquear basado en él. Lo que bloquea es un freno aparte (clase anterior de la ruta); este hook solo observa.

Pruébate

¿Este hook de registro debe bloquear el comando antes de que se ejecute?

Practica ahora 0/4 hecho

Registro fechado de cada comando

Registra, con la hora, cada comando de Bash que Claude ejecute en esta sesión. ~5 min, seguro (solo observa, siempre termina con exit 0).

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r '(now|gmtime|strftime(\"%Y-%m-%d %H:%M:%S\")) + \" \" + .tool_input.command' >> \"$CLAUDE_PROJECT_DIR/.claude/cmd.log\"; exit 0" }
        ]
      }
    ]
  }
}

✓ Cierre: ahora existe un diario fechado de todo lo que ejecutó la sesión — auditable, sin bloquear nada.

Ruta 03 · Lección 4 · Intermedio

Inyectar contexto
en el inicio

SessionStart y UserPromptSubmit entregan contexto directamente a Claude — branch, git status, reglas del proyecto — sin que tengas que pegar nada.

↓ desplázate para estudiar

01 SessionStart y UserPromptSubmit

SessionStart se activa una vez, cuando se inicia la sesión. UserPromptSubmit se activa la cada mensaje que envías. Ninguno de los dos es un evento de herramienta: no usan matcher, simplemente se activan en el momento adecuado.

Ambos pueden devolver contexto adicional para Claude. La diferencia es la frecuencia: el contexto de sesión (rama, reglas del proyecto) va en el SessionStart; algo que cambia con cada mensaje entraría en el UserPromptSubmit — con moderación, para no repetirlo todo el tiempo.

Pruébate

¿Qué evento se activa UNA VEZ, al inicio de la sesión?

02 additionalContext dentro de hookSpecificOutput

Para tomar decisiones sobre herramientas, el JSON puede ser aún más compacto con un bloque propio: JSON estructurado en stdout, con el texto dentro de hookSpecificOutput, campo additionalContext:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Branch: main\nMudanças: 2 arquivos"
  }
}

Este texto —y solo este— llega a Claude como contexto adicional. No lo imprimas sin más en stdout esperando que «aparezca»; el formato importa.

Pruébate

¿Dónde está el texto que el hook inyecta como contexto para Claude?

03 Inyectar git status / CLAUDE.md / TODO

Combina las fuentes que importan: la rama actual, lo que cambió, quizás una línea que diga que existe un CLAUDE.md con reglas. Construye la cadena y escápala con jq -Rs para convertirlo en un valor JSON válido:

# .claude/hooks/session-context.sh (SessionStart)
branch=$(git branch --show-current)
status=$(git status --short)
ctx="Branch: $branch"$'\n'"Mudanças:"$'\n'"$status"
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}' \
  "$(printf '%s' "$ctx" | jq -Rs .)"

O jq -Rs . lee texto sin formato (no JSON) y devuelve una string JSON ya escapada — incluidos los saltos de línea y las comillas. Sin eso, un \n suelto rompería el JSON de salida.

04 Contexto adecuado y breve

La tentación es poner todo el proyecto en el additionalContext. Resístete: demasiado contexto ensucia y desperdicia espacio que Claude usaría para pensar en tu solicitud. Inyecta solo lo que cambia la decisión de él ahora — branch, qué está sucio, una o dos líneas de regla — no todo el historial.

Regla práctica: si no copiarías eso manualmente en un mensaje, tampoco lo inyectes automáticamente.

Profundiza

Combina los cuatro hooks de la ruta y el entorno se cuida solo: formatea al guardar, avisa cuando termina, registra lo que se ejecutó y entrega el contexto adecuado en cuanto se abre la sesión.

Pruébate

¿Por qué no incluir todo el contenido del proyecto en additionalContext todo el tiempo?

Practica ahora 0/4 hecho

SessionStart inyecta branch + git status · capstone

Haz que la sesión se abra sabiendo ya cuál es la rama y qué cambió en el proyecto, sin que tengas que contar nada. ~8 min.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "bash .claude/hooks/session-context.sh" }
        ]
      }
    ]
  }
}

✓ Capstone: la sesión se abre sabiendo ya del proyecto — la rama, los cambios, todo eso sin que pegues nada. Fin de la ruta de automatización.