Claude

Hooks en Claude Code: automatiza formato, bloqueos y avisos

Los hooks son comandos que Claude Code ejecuta en momentos concretos de su ciclo de vida. Te explico cómo crearlos, filtrarlos con matchers y usarlos para formatear código, proteger archivos y recibir avisos.

  • Javi Mata
  • 8 min de lectura
Tabla de contenido

Puedes pedirle a Claude Code, en tu CLAUDE.md, que “siempre formatee el código con Prettier”. Casi siempre lo hará, pero casi siempre no es siempre. Los hooks resuelven eso: son comandos de shell definidos por ti que Claude Code ejecuta en puntos específicos de su ciclo de vida, de modo que ciertas acciones ocurren siempre y no dependen de que el modelo decida hacerlas.

Si aún no usas Claude Code, empieza por la guía de inicio. Para dar instrucciones que sí dependen del criterio de Claude, mira cómo crear un skill.

Tu primer hook: un aviso cuando Claude te necesita

Los hooks se declaran en un bloque hooks dentro de un archivo de configuración. Este ejemplo lanza una notificación de escritorio cuando Claude está esperando tu respuesta, para que no tengas que vigilar la terminal. Abre ~/.claude/settings.json (créalo si no existe) y añade un hook de tipo Notification. Este ejemplo usa osascript, así que es para macOS:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Si tu archivo ya tiene una clave hooks, agrega Notification como hermana de los eventos existentes; no reemplaces todo el objeto. Para Linux, la documentación usa notify-send 'Claude Code' 'Claude Code needs your attention'.

Para comprobar que quedó registrado, escribe /hooks dentro de Claude Code. Verás la lista de eventos con un contador junto a los que tienen hooks configurados. Ojo: ese menú es de solo lectura; para añadir, cambiar o quitar hooks edita el JSON (o pídeselo a Claude).

Si en macOS no aparece la notificación, la documentación explica que osascript la enruta por la app Script Editor. Ejecuta osascript -e 'display notification "test"' una vez, abre Ajustes del Sistema > Notificaciones y activa las notificaciones de Script Editor.

Los eventos más útiles

Cuando ocurre un evento, Claude Code ejecuta en paralelo todos los hooks que coinciden. Estos son algunos de los eventos que documenta la guía:

EventoCuándo se dispara
SessionStartAl empezar o reanudar una sesión
UserPromptSubmitAl enviar un prompt, antes de que Claude lo procese
PreToolUseAntes de ejecutar una herramienta. Puede bloquearla
PermissionRequestCuando una herramienta necesita una decisión de permiso
PostToolUseDespués de que una herramienta se ejecuta con éxito
NotificationCuando Claude Code envía una notificación
StopCuando Claude termina de responder
PreCompact / PostCompactAntes y después de compactar el contexto
SessionEndCuando termina la sesión

La lista completa es más larga (incluye eventos de subagentes, cambios de configuración, cambios de directorio y más); está en la referencia de hooks.

Formatear con Prettier después de cada edición

Este hook usa PostToolUse con el matcher Edit|Write, así que solo corre después de las herramientas que editan archivos. Extrae la ruta con jq y se la pasa a Prettier. Guárdalo en .claude/settings.json en la raíz del proyecto:

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

Necesitas jq instalado (brew install jq en macOS). Para probarlo, pídele a Claude que agregue una línea con comillas simples a un archivo JavaScript y ábrelo: con la configuración por defecto de Prettier, el hook las cambia a comillas dobles. Cuando un hook tiene éxito, Claude Code no muestra nada en la conversación; lo confirmas viendo el archivo reformateado.

Bloquear ediciones a archivos protegidos

Un PreToolUse puede impedir que Claude toque archivos sensibles como .env, package-lock.json o cualquier cosa dentro de .git/. El hook llama a un script; si el script termina con código de salida 2, Claude Code bloquea la acción y le pasa a Claude el mensaje de stderr para que ajuste su enfoque.

Guarda esto en .claude/hooks/protect-files.sh:

#!/bin/bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

exit 0

Hazlo ejecutable en macOS y Linux, porque Claude Code necesita poder ejecutarlo:

chmod +x .claude/hooks/protect-files.sh

Y regístralo en .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
          }
        ]
      }
    ]
  }
}

Para probarlo, pídele a Claude que añada un comentario a tu .env: Claude Code bloquea la edición y le pasa el mensaje Blocked: como retroalimentación.

Cómo se comunica un hook con Claude Code

Claude Code le pasa al script los datos del evento como JSON por stdin. Para un PreToolUse de Bash, el JSON incluye hook_event_name, tool_name y tool_input (donde command trae el comando de shell). El script responde con el código de salida:

  • Exit 0: no hay objeción. En un PreToolUse esto no aprueba la herramienta: sigue el flujo normal de permisos.
  • Exit 2: Claude Code bloquea la acción. Escribe el motivo en stderr.
  • Cualquier otro código: para la mayoría de eventos es un error no bloqueante y la acción continúa (hay excepciones por evento, documentadas en la referencia).

Si necesitas más control que bloquear o no bloquear, sal con código 0 e imprime un objeto JSON en stdout. Por ejemplo, un PreToolUse puede denegar una llamada y explicarle a Claude por qué:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Use rg instead of grep for better performance"
  }
}

La documentación recomienda elegir un enfoque por hook (código 2 con mensaje, o código 0 con JSON), no mezclarlos. Y un detalle importante: los campos como permissionDecision van dentro de hookSpecificOutput; si los pones al nivel superior, Claude Code los ignora sin avisar.

Matchers: no dispares el hook en todo

Sin matcher, un hook se dispara en cada ocurrencia de su evento. Cada evento filtra por un campo distinto:

EventoEl matcher filtra porEjemplos
PreToolUse, PostToolUse, PermissionRequestNombre de la herramientaBash, Edit|Write, mcp__.*
SessionStartCómo empezó la sesiónstartup, resume, clear, compact, fork
NotificationTipo de notificaciónpermission_prompt, idle_prompt

Los matchers distinguen mayúsculas de minúsculas. Las herramientas de MCP tienen la forma mcp__<servidor>__<herramienta>, por ejemplo mcp__github__search_repositories; si conectaste servidores como en esta guía de MCP, puedes apuntar a todas las herramientas de un servidor con un patrón como mcp__github__.*.

Los eventos UserPromptSubmit, Stop y algunos más no admiten matcher: se disparan siempre.

Filtrar por argumentos con if

El campo if usa la sintaxis de las reglas de permisos para filtrar por herramienta y argumentos, de modo que el proceso del hook solo se lanza cuando la llamada coincide. Por ejemplo, para correr un script solo cuando Claude usa comandos git:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
          }
        ]
      }
    ]
  }
}

if solo funciona en eventos de herramientas (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied). Además, es un filtro de mejor esfuerzo: la propia documentación recomienda usar el sistema de permisos, no un hook, cuando necesites una regla estricta de permitir o denegar.

Dónde guardar un hook

UbicaciónAlcance¿Se comparte?
~/.claude/settings.jsonTodos tus proyectosNo, es local
.claude/settings.jsonUn proyectoSí, puedes hacer commit
.claude/settings.local.jsonUn proyectoNo, se ignora en git

También pueden vivir en configuración administrada, en plugins y en el frontmatter de skills y subagentes. Para desactivarlos todos, existe "disableAllHooks": true.

Cuando algo no funciona

  • El hook no se dispara: ejecuta /hooks y confirma que aparece bajo el evento correcto. Revisa que el matcher coincida exactamente con el nombre de la herramienta.
  • “hook error” en la conversación: prueba el script a mano con un JSON de ejemplo: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh y luego echo $?.
  • “command not found”: usa rutas absolutas o $CLAUDE_PROJECT_DIR. Si ves jq: command not found, instala jq.
  • El script no arranca: hazlo ejecutable con chmod +x.
  • Tu JSON no tiene efecto y no hay error: suele haber texto extra antes del JSON. La causa habitual es un echo incondicional en tu ~/.zshrc o ~/.bashrc. Envuélvelo para que solo corra en shells interactivos:
# In ~/.zshrc or ~/.bashrc
if [[ $- == *i* ]]; then
  echo "Shell ready"
fi
  • Un hook Stop que no deja parar a Claude: Claude Code lo anula tras bloquear ocho veces seguidas sin avance. Revisa el campo stop_hook_active del JSON de entrada y sal con 0 si es true.

Para ver qué hooks coincidieron, sus códigos de salida y su salida, inicia con claude --debug-file /tmp/claude.log y sigue el archivo con tail -f /tmp/claude.log. En una sesión ya abierta, /debug activa el registro.

Mi recomendación

Mi recomendación es empezar con dos hooks pequeños: el aviso de Notification y el formateo con PostToolUse. Son inocuos, se notan desde el primer día y te enseñan el patrón. Deja los hooks de bloqueo para reglas que de verdad no debas romper, y para todo lo demás usa CLAUDE.md o un skill. Y ten presente que un PostToolUse no puede deshacer lo que la herramienta ya hizo: el bloqueo se hace antes, con PreToolUse.

Conclusión

Los hooks convierten “por favor, hazlo siempre” en “ocurre siempre”. Con un evento, un matcher y un comando de shell puedes formatear, proteger archivos, avisarte o inyectar contexto sin depender de la memoria del modelo.

Fuentes