Claude Code Hooks v2 en LatAm: 5 usos y 30+ eventos
Escribí en mi CLAUDE.md, tres veces y con tres redacciones distintas, que no tocara los archivos .env. Claude estuvo de acuerdo las tres veces, con mucha educación. A la cuarta sesión, abrió uno y lo editó.
No fue mala fe: fue lo que pasa cuando una regla vive en la memoria del modelo. Una instrucción en CLAUDE.md es una petición, y una petición se cumple casi siempre. Pero ese “casi” es el 5 por ciento que termina en incidente.
Los hooks son la contraparte: no son una petición, son código. Cuando Claude Code intenta ejecutar una herramienta, el hook se dispara antes o después, siempre, porque está programado para hacerlo. Hooks v2 subió la apuesta: hoy hay más de 30 eventos documentados en el ciclo de vida de una sesión y prácticamente cualquiera puede convertirse en un script.
Esta guía tiene tres partes: los 6 grupos de eventos que forman el mapa completo, los 5 hooks que yo dejo activos en mi computadora, y los 3 errores donde perdí una tarde antes de leer bien la doc.
CLAUDE.md vs Hooks: la diferencia en una frase
CLAUDE.md le habla al modelo. Los hooks le hablan al runtime.
CLAUDE.md entra al contexto y Claude lo interpreta como una intención. Ese contexto se olvida, se comprime, o compite con la petición nueva del usuario. Un hook, en cambio, es una orden que se evalúa en cada intento de herramienta. No depende de que el modelo recuerde nada.
Es la diferencia entre un cartel de “no entrar” y una cerradura. El cartel funciona casi siempre. La cerradura funciona el 100 por ciento de las veces, también cuando nadie está mirando.
Los 30+ eventos, en 6 grupos
La referencia oficial enumera hoy 31 eventos. Memorizarlos no sirve de nada, agruparlos sí. Uso este mapa mental cuando busco dónde engancharme.
| Grupo | Eventos | Para qué se usan |
|---|---|---|
| Ciclo de sesión | SessionStart, Setup, SessionEnd, InstructionsLoaded | Preparar contexto al arrancar, limpiar al salir |
| Interacción con el usuario | UserPromptSubmit, UserPromptExpansion, Stop, StopFailure | Reescribir o auditar prompts, cerrar el turno |
| Ejecución de herramientas | PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PermissionRequest, PermissionDenied, Notification | Bloquear, formatear o registrar cada uso de herramienta |
| Subagentes y tareas | SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle | Observar el trabajo delegado a otros agentes |
| Archivos y entorno | FileChanged, DirectoryAdded, CwdChanged, ConfigChange, WorktreeCreate, WorktreeRemove | Reaccionar a cambios fuera del turno de Claude |
| Contexto y MCP | PreCompact, PostCompact, Elicitation, ElicitationResult, MessageDisplay | Enganchar compactación de contexto y protocolos MCP |
Sólo un puñado bloquea la ejecución (los del grupo de herramientas y PreCompact). El resto sirve para observar, registrar o inyectar contexto. Saber cuál bloquea y cuál no evita el error clásico de “escribí un hook y no pasó nada”.
Estructura mínima de un hook
Los hooks se declaran en settings.json con tres capas: el nombre del evento, el matcher que decide cuándo dispara, y la lista de handlers que se ejecutan.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo hola",
"timeout": 30
}
]
}
]
}
}
El matcher acepta strings exactos ("Bash", "Edit|Write") o expresiones regulares cuando lleva caracteres especiales. Para filtrar por argumentos —no sólo por nombre de herramienta— existe un segundo campo, if, que usa la sintaxis de las reglas de permisos: "Bash(git *)" o "Edit(*.ts)".
El handler command recibe un JSON en stdin con toda la información del evento. Esto es clave y es donde tropecé la primera vez.

Los 5 hooks que uso en producción
1. Bloquear git push --force
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git push --force*)",
"command": "echo 'force push bloqueado' >&2; exit 2"
}
]
}
]
}
}
exit 2 es lo que bloquea la herramienta. exit 0 deja pasar y exit 1 sólo registra un aviso en el log. El texto que va a stderr vuelve a Claude como razón, así el modelo no se queda dando vueltas intentando lo mismo.
2. Proteger archivos .env
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"if": "Edit(*.env*)|Write(*.env*)",
"command": "echo '.env es solo lectura' >&2; exit 2"
}
]
}
Mismo patrón. if filtra por la extensión del archivo antes de que la herramienta corra. Sin if, el hook se dispararía en cada Edit y se convertiría en ruido.
3. Formatear al guardar (leyendo stdin JSON, no un env var mágica)
Este es el hook que me costó una tarde. Yo daba por hecho que existía una variable $FILE_PATH con la ruta del archivo editado. No existe. La ruta se pasa dentro del JSON que llega por stdin.
Primero, el handler en settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format-file.sh",
"timeout": 30,
"statusMessage": "Formateando…"
}
]
}
]
}
}
${CLAUDE_PROJECT_DIR} sí es una variable exportada al proceso del hook: apunta a la raíz del proyecto donde arrancó la sesión. Ahí guardo el script.
Después, el script que hace el trabajo:
#!/usr/bin/env bash
# .claude/hooks/format-file.sh
set -euo pipefail
# El JSON del evento entra por stdin. Extraigo la ruta con jq.
file_path=$(jq -r '.tool_input.file_path // empty')
# Si el evento no trae file_path, salgo sin hacer nada.
[[ -z "$file_path" ]] && exit 0
case "$file_path" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.md)
npx --no prettier --write "$file_path"
;;
esac
La regla de dedo: si necesitas un dato del evento (nombre de herramienta, argumento, ruta, cwd), viene por stdin como JSON. Las únicas variables de entorno documentadas hoy son CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_EFFORT y un par más para el modo remoto. Todo lo demás sale del JSON.
4. Inyectar contexto al iniciar sesión
SessionStart no acepta variables mágicas para “exportar” env vars a Claude. Lo que hace, según la doc, es tomar el stdout del hook como contexto que Claude puede leer al arrancar la sesión.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-context.sh"
}
]
}
]
}
}
Y el script:
#!/usr/bin/env bash
# .claude/hooks/session-context.sh
set -euo pipefail
echo "Rama activa: $(git rev-parse --abbrev-ref HEAD)"
echo "Últimos 3 commits:"
git log --oneline -3
echo "Flags del proyecto (.env.local no incluido)"
Cuando Claude arranca, ese texto entra al contexto inicial. Ya no le tengo que contar en qué rama estoy ni cuál fue el último cambio. Nota: SessionStart no puede bloquear la sesión, sólo informar.
5. Auditar el acceso a secretos con permissionDecision
Para reglas donde quiero rechazar con una razón visible, en vez de exit 2 uso la forma declarativa: el hook imprime un JSON en stdout con hookSpecificOutput.permissionDecision.
#!/usr/bin/env bash
# .claude/hooks/audit-secrets.sh
set -euo pipefail
payload=$(cat)
cmd=$(echo "$payload" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -Eq '(AWS_SECRET|OPENAI_API_KEY|STRIPE_LIVE)'; then
echo "$cmd" >> "${CLAUDE_PROJECT_DIR}/.claude/audit.jsonl"
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Acceso a variables de secretos bloqueado por política"
}
}
JSON
exit 0
fi
permissionDecision: "deny" es el equivalente declarativo del exit 2 y además permite explicar la razón. Un detalle importante: en PreToolUse, la decisión va dentro de hookSpecificOutput.permissionDecision, no como decision en la raíz. Ese es el error 3 de la sección siguiente.
Los 3 errores que descubrí en el proceso
Error 1: buscar $FILE_PATH como variable de entorno.
No existe. El único set de variables exportadas al proceso del hook es el que menciona la doc (CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_EFFORT, CLAUDE_CODE_REMOTE, CLAUDE_CODE_BRIDGE_SESSION_ID). Todo lo demás llega en el JSON de stdin. Solución: extraerlo con jq -r '.tool_input.file_path'.
Error 2: pensar que exit 1 bloquea.
exit 1 sólo deja constancia del reclamo. La herramienta se ejecuta igual. Sólo exit 2 bloquea y devuelve stderr al modelo. Es un dígito, pero es toda la diferencia entre “queda anotado” y “hay una pared”.
Error 3: mezclar el schema de decisión.
PreToolUse usa hookSpecificOutput.permissionDecision ("deny", "allow", etc.). Otros eventos usan otras formas de salida. Si copias el JSON de un ejemplo de otro evento tal cual, tu decisión no se dispara y vuelves a estar en el cartel de papel.
Los tres se resuelven leyendo la referencia de hooks despacio. Yo pagué la tarde por no hacerlo.
Nota LatAm
Estos ejemplos los corro en macOS y Linux, que es lo que tengo. Si desde LatAm los quieres montar en tu setup, dos cosas que suelen romper:
- Rutas con espacios o acentos en
~/Documentos/…: los scripts asumenset -euo pipefaily quotear"$file_path"es obligatorio. jqno viene por defecto en algunas distros:sudo apt install jqen Debian/Ubuntu,brew install jqen macOS,sudo dnf install jqen Fedora. Windows/WSL: dentro de WSL, mismo comando que Ubuntu.
Yo trabajo como indie desde Japón, así que no puedo hablar de cómo se comportan estos hooks en un pipeline corporativo grande. La estructura es la misma; lo que cambia es cuántos permisos previos les den en tu organización antes de tocar settings.json.
Cierre
Los 30+ eventos de Hooks v2 son un buffet, no una tarea escolar. No hay que probarlos todos. Elige uno del grupo de “ejecución de herramientas”, conviértelo en exit 2 para una regla que hoy vive en tu CLAUDE.md, y observa cómo esa regla deja de “casi siempre” cumplirse.
Los tres errores que me costaron la tarde ya están arriba. Si al menos te ahorro esos tres, esta guía ya pagó su lectura.
Recursos
- Referencia oficial de Hooks (code.claude.com): fuente primaria, la que tenía que haber leído antes de perder la tarde
- Por qué Claude ignora tu CLAUDE.md 1 de cada 20 veces (y cómo lo arreglé con exit code 2): mi post anterior sobre el mismo tema, centrado en el dígito que separa
exit 1deexit 2 - Los 7 archivos que Claude Code lee al arrancar: dónde encaja
settings.jsonen el resto del contexto
Si quieres el material largo con capítulos sobre CLAUDE.md, Plan Mode, subagentes y patrones de equipo, lo dejé en Practical Claude Code: La Ingeniería de Contexto que Transforma tu Desarrollo. Está en Kindle Unlimited.
ken imoto · WebRTC & Voice AI engineer · kenimoto.dev
Libro relacionado Practical Claude Code Claude Code desde cero hasta producción — CLAUDE.md, Plan Mode y workflows de equipo, desde un año de uso real Ver la página del libro → ¿Te resultó útil este artículo?