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
- Tu primer hook: un aviso cuando Claude te necesita
- Los eventos más útiles
- Formatear con Prettier después de cada edición
- Bloquear ediciones a archivos protegidos
- Cómo se comunica un hook con Claude Code
- Matchers: no dispares el hook en todo
- Filtrar por argumentos con if
- Dónde guardar un hook
- Cuando algo no funciona
- Mi recomendación
- Conclusión
- Fuentes
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:
| Evento | Cuándo se dispara |
|---|---|
SessionStart | Al empezar o reanudar una sesión |
UserPromptSubmit | Al enviar un prompt, antes de que Claude lo procese |
PreToolUse | Antes de ejecutar una herramienta. Puede bloquearla |
PermissionRequest | Cuando una herramienta necesita una decisión de permiso |
PostToolUse | Después de que una herramienta se ejecuta con éxito |
Notification | Cuando Claude Code envía una notificación |
Stop | Cuando Claude termina de responder |
PreCompact / PostCompact | Antes y después de compactar el contexto |
SessionEnd | Cuando 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
PreToolUseesto 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:
| Evento | El matcher filtra por | Ejemplos |
|---|---|---|
PreToolUse, PostToolUse, PermissionRequest | Nombre de la herramienta | Bash, Edit|Write, mcp__.* |
SessionStart | Cómo empezó la sesión | startup, resume, clear, compact, fork |
Notification | Tipo de notificación | permission_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ón | Alcance | ¿Se comparte? |
|---|---|---|
~/.claude/settings.json | Todos tus proyectos | No, es local |
.claude/settings.json | Un proyecto | Sí, puedes hacer commit |
.claude/settings.local.json | Un proyecto | No, 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
/hooksy 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.shy luegoecho $?. - “command not found”: usa rutas absolutas o
$CLAUDE_PROJECT_DIR. Si vesjq: command not found, instalajq. - 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
echoincondicional en tu~/.zshrco~/.bashrc. Envuélvelo para que solo corra en shells interactivos:
# In ~/.zshrc or ~/.bashrc
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
- Un hook
Stopque no deja parar a Claude: Claude Code lo anula tras bloquear ocho veces seguidas sin avance. Revisa el campostop_hook_activedel JSON de entrada y sal con 0 si estrue.
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
Etiquetas:
Compartir: