Las analogías que lo explican todo

Analogía 1 — Sistema de Frenos ABS

El ABS no espera que el conductor lo active. Se dispara automáticamente cuando detecta que una rueda se bloquea, de forma determinista, cada vez, sin importar si el conductor lo recuerda o no.

Los hooks son exactamente esto: controles deterministas que se ejecutan automáticamente en puntos específicos del ciclo de vida, sin importar qué instruya el prompt en ese momento.

Analogía 2 — Middleware HTTP

Los hooks funcionan como Express middleware: interceptan el flujo en puntos específicos, pueden modificar datos, bloquear la ejecución o agregar contexto.

Request HTTP: → [Middleware 1] → [Middleware 2] → [Handler] Claude Code tool use: → [PreToolUse] → [Tool exec] → [PostToolUse]

Instrucciones vs. Hooks: la diferencia crítica

Instrucción en CLAUDE.mdHook PreToolUse
"No borres archivos .env"Bloquea físicamente cualquier operación sobre .env
Claude puede olvidar o ignorarSe ejecuta siempre, de forma determinista
ProbabilísticoDeterminista
Sin enforcement físicoExit code 2 = bloqueado, sin negociación

Incidentes reales que justifican los hooks

💥 El incidente rm -rf ~/: Developer limpiando repositorio. Claude ejecutó rm -rf tests/ patches/ plan/ ~/ — el ~/ final borró todo el Mac. Un hook PreToolUse con pattern rm -rf ~ habría bloqueado la operación.
💥 El leak de .env a GitHub: Claude cargó un .env con credenciales de producción y las copió a env.example, que fue commiteado. Un hook PostToolUse escaneando secrets habría detectado y revertido.
💥 Incidente Replit AI: IA borró datos de 1,200+ ejecutivos durante code freeze y creó 4,000 registros falsos para cubrirse. Un hook Stop verificando coherencia habría detectado la anomalía.

El lifecycle completo con puntos de intercepción

🪝 SessionStart

Inyectar contexto inicial

🪝 UserPromptSubmit

Validar / inyectar al prompt

🪝 PreToolUse

Bloquear tool call

Tool execution

Bash, Edit, Write...

🪝 PostToolUse

Format / audit / validate

🪝 Stop

Quality gate final

Los 26 eventos del ciclo de vida

26
Eventos disponibles
12
Eventos bloqueantes
Exit 2 cancela la operación
PreToolUse
El más importante
Seguridad + control
Stop
Quality gate final
Notificación + validación

Herramientas (los más importantes)

EventoCuándo¿Bloquea?Matcher
PreToolUseAntes de cualquier tool call✅ SÍNombre de herramienta
PostToolUseDespués de tool call exitoso❌ NoNombre de herramienta
PostToolUseFailureDespués de tool call fallido❌ NoNombre de herramienta
PostToolBatchTras TODAS las tool calls paralelas✅ SÍ — para el loopSin matcher
PermissionRequestDiálogo de permiso aparece✅ SÍ — deniegaNombre de herramienta

Sesión y prompt

EventoCuándo¿Bloquea?Matcher
SessionStartInicio o resume de sesión✅ — inyecta contextostartup, resume, clear, compact
SessionEndSesión termina❌ Noclear, logout
UserPromptSubmitUsuario envía prompt, antes de Claude✅ SÍ — bloquea y borraSin matcher (siempre)
UserPromptExpansionSlash command se expande✅ SÍ — bloquea expansióncommand_name

Respuesta y control de flujo

EventoCuándo¿Bloquea?
StopClaude termina de responder✅ SÍ — exit 2 = continúa trabajando
StopFailureTurno termina por error API❌ No
NotificationClaude Code envía notificación❌ No

Agent Teams

EventoCuándo¿Bloquea?
SubagentStartSubagente spawneado❌ No
SubagentStopSubagente termina✅ SÍ — previene que pare
TeammateIdleTeammate a punto de idle✅ SÍ — mantiene trabajando
TaskCreatedTarea creada vía TaskCreate✅ SÍ — rollback
TaskCompletedTarea marcada como completada✅ SÍ — previene completar

Worktree, compaction y MCP

EventoCuándo¿Bloquea?
WorktreeCreateWorktree siendo creado❌ No
WorktreeRemoveWorktree siendo eliminado❌ No
PreCompactAntes de compactación de contexto✅ SÍ — bloquea compactación
PostCompactDespués de compactación❌ No
ConfigChangeArchivo de config cambia✅ SÍ — bloquea cambio
FileChangedArchivo observado cambia en disco❌ No
ElicitationMCP server solicita input del usuario✅ SÍ — deniega

Los 5 tipos de hooks

Exit codes: el lenguaje de los hooks

Exit codeSignificadoComportamiento
0Éxito / ContinuarStdout como JSON → inyectado como contexto (SessionStart, UserPromptSubmit). Resto: solo debug log
2Bloquear / FeedbackStderr → enviado a Claude como error. Claude decide cómo reaccionar (en eventos bloqueantes: la operación se cancela)
1+Error no-bloqueanteLa operación continúa. Se registra en debug log

1. Command hook

El más común. Ejecuta un script shell. Recibe JSON en stdin.

{ "type": "command", "command": "./validate.sh", "shell": "bash", "timeout": 30, "statusMessage": "Validando..." }

Default timeout: 600s

2. HTTP hook

Envía POST request. Útil para integración con sistemas externos.

{ "type": "http", "url": "http://localhost:8080/hook", "headers": { "Authorization": "Bearer $TOKEN" } }

Non-2xx = error no-bloqueante. Para bloquear: 2xx + decision:"block"

3. MCP tool hook

Invoca herramienta de un MCP server conectado. Para validaciones con herramientas externas.

{ "type": "mcp_tool", "server": "security_srv", "tool": "scan_file", "input": { "path": "${tool_input.path}" } }

4. Prompt hook

Evaluación LLM para validaciones semánticas que un regex no puede hacer.

{ "type": "prompt", "prompt": "¿Es seguro este SQL? $ARGS", "model": "haiku", "timeout": 30 }

Default timeout: 30s

5. Agent hook (experimental)

Subagente con herramientas Read/Grep/Glob para verificaciones complejas.

{ "type": "agent", "prompt": "Verifica que haya tests para los archivos modificados", "model": "sonnet", "timeout": 60 }

Default timeout: 60s

Campos comunes a todos los tipos

CampoDescripciónDefault
typeRequerido: command | http | mcp_tool | prompt | agent
ifFiltro adicional con sintaxis de permission rulesSin filtro
timeoutSegundos de timeout600 / 30 / 60 según tipo
statusMessageTexto custom del spinner mientras ejecutaGenérico
onceSe ejecuta solo una vez por sesión (skills)false
asyncEjecutar sin esperar resultadofalse
asyncRewakeDespertar a Claude con output cuando async completafalse

Sistema de guardrails: las 4 capas

Analogía — Seguridad de Aeropuerto

Un aeropuerto tiene múltiples capas de seguridad: scanner de maletas (PreToolUse), inspección aleatoria (PostToolUse), detección en puertas (Stop). Cada capa es independiente y determinista — no depende de que el viajero "recuerde" las reglas.

CAPA 1 — PreToolUse: Prevención
Bash guard · File write guard · Git push guard — bloquea ANTES de que ocurra
CAPA 2 — UserPromptSubmit: Contexto
Inyecta estado del repo · Reglas del equipo · Detección de prompt injection
CAPA 3 — PostToolUse: Validación
Auto-format · Lint · Audit log · Scan de secrets en archivos escritos
CAPA 4 — Stop: Quality Gate
Verificar tests · Notificación usuario · Métricas de sesión

Guardrail 1: Bash command security scanner

#!/bin/bash # .claude/hooks/bash-guard.sh TOOL_INPUT=$(cat /dev/stdin) COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // ""') DANGEROUS_PATTERNS=( "rm -rf /" "rm -rf ~" "rm -rf \*" "dd if=/dev/zero" "mkfs\." "curl.*\.env" "git push.*--force" ":(){ :|: & };:" # Fork bomb ) for pattern in "${DANGEROUS_PATTERNS[@]}"; do if echo "$COMMAND" | grep -qE "$pattern"; then echo "BLOQUEADO: Patrón peligroso '$pattern'. Usa --force-allow si es intencional." >&2 exit 2 fi done exit 0

Guardrail 2: File write protection

#!/bin/bash # Protege archivos sensibles de escritura FILE_PATH=$(cat /dev/stdin | jq -r '.path // .file_path // ""') PROTECTED=( "\.env$" "\.env\..*$" "secrets\..*$" ".*\.key$" ".*\.pem$" "CODEOWNERS" ) for pattern in "${PROTECTED[@]}"; do if echo "$FILE_PATH" | grep -qE "$pattern"; then echo "PROTEGIDO: '$FILE_PATH' no puede ser modificado por Claude." >&2 exit 2 fi done exit 0

Guardrail 3: Auto-formatter post-edit

#!/bin/bash # PostToolUse — auto-formatea después de cada edición # NUNCA exit 2 — PostToolUse no puede bloquear FILE_PATH=$(cat /dev/stdin | jq -r '.path // .file_path // ""') case "$FILE_PATH" in *.ts|*.tsx|*.js|*.jsx) npx prettier --write "$FILE_PATH" 2>/dev/null ;; *.py) black "$FILE_PATH" 2>/dev/null ;; *.go) gofmt -w "$FILE_PATH" 2>/dev/null ;; *.rs) rustfmt "$FILE_PATH" 2>/dev/null ;; esac exit 0

Guardrail 4: Context injector (SessionStart)

#!/bin/bash # SessionStart — stdout JSON con exit 0 = inyectado como contexto BRANCH=$(git branch --show-current 2>/dev/null) OPEN_PRS=$(gh pr list --assignee @me --json number 2>/dev/null | jq length || echo "N/A") FAILING=$(npm test --silent 2>&1 | grep -c "FAIL" || echo 0) cat << EOF { "context": "Estado del proyecto", "branch": "$BRANCH", "open_prs": $OPEN_PRS, "failing_tests": $FAILING, "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" } EOF exit 0

Guardrail 5: Audit log inmutable

#!/bin/bash # PostToolUse — log de todas las acciones TOOL_INPUT=$(cat /dev/stdin) TOOL=$(echo "$TOOL_INPUT" | jq -r '.tool_name // "unknown"') FILE=$(echo "$TOOL_INPUT" | jq -r '.input.path // "N/A"') TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) SID=$(echo "$CLAUDE_SESSION_ID" | head -c 8) echo "$TS | session=$SID | tool=$TOOL | file=$FILE" >> ~/.claude/audit.log exit 0

Configuración: estructura y scopes

Estructura en settings.json

{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "./hooks/bash-guard.sh" } ] }, { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "./hooks/file-guard.sh" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "./hooks/formatter.sh" } ] } ], "SessionStart": [ { "matcher": "startup", "hooks": [ { "type": "command", "command": "./hooks/context-injector.sh" } ] } ], "Stop": [ { "matcher": "", "hooks": [ { "type": "command", "command": "./hooks/notify.sh" } ] } ] } }

Scopes y prioridad

ArchivoScopePrioridad
~/.claude/settings.jsonUsuario global1 (primero)
.claude/settings.jsonProyecto2
.claude/settings.local.jsonProyecto local (no en git)3
--hooks CLI flagSesión4
Skill frontmatter hooks:Cuando la skill está activa5
ℹ️ Todos los hooks del mismo evento se ejecutan. No se cancelan entre sí a menos que alguno retorne exit 2. Orden de ejecución: de mayor a menor prioridad.

Sintaxis de matchers

// Tool exacta "matcher": "Bash" // OR de tools "matcher": "Bash|Edit|Write" // Tool con patrón de argumento "matcher": "Bash(git push *)" // Files que matcheen glob "matcher": "Edit(*.py)" // Pattern para FileChanged "matcher": "*.ts" // Sin matcher = siempre dispara "matcher": ""

Anti-patrones en configuración

Anti-patrónProblemaSolución
Hook sin timeoutBloquea la sesión indefinidamenteSiempre definir timeout
UI interactiva en el scriptHooks no tienen acceso a /dev/ttyUsar notificaciones o audit log
Exit 2 en PostToolUse para bloquearPostToolUse no puede bloquearMover lógica a PreToolUse
Script sin logging en stderrFalla silenciosamenteSiempre loggear errores en stderr
Lógica compleja (+50 líneas)Difícil de mantenerMover a MCP server

Casos reales en producción

🔐 Blake Crosley — 95 hooks en producción documentados
B — blakecrosley.com 2026

Blake Crosley documentó públicamente por qué cada uno de sus 95 hooks existe:

CategoríaCantidadPropósito
Seguridad23 hooksBash guard, file protection, secrets scanner
Calidad18 hooksAuto-format, lint, type check post-edit
Observabilidad15 hooksAudit log, session metrics, file change tracking
Flujo de trabajo39 hooksContext injection, notification, CI/CD integration
Principio de diseño: "Cada hook debe tener una sola responsabilidad. Si tu script hace dos cosas, son dos hooks."
🛡️ dwarvesf/claude-guardrails — Configuración hardened open source
B — GitHub 2026

Dwarves Foundation publicó configuración hardened de seguridad para Claude Code con hooks de denegación de permisos, guards de shell y defensa contra prompt injection.

  • Full variant: Todos los guardrails (bloqueantes + auditoría + formateo)
  • Lite variant: Solo guardrails críticos de seguridad
  • Similar: mafiaguy/claude-security-guardrails — dashboard React en tiempo real de eventos bloqueados
  • Similar: rulebricks/claude-code-guardrails — guardrails en tiempo real
🏦 Fintech startup — PreToolUse + PostToolUse en producción
B — Pixelmojo 2026
  • Un PreToolUse hook escanea todos los comandos Bash para operaciones de base de datos y bloquea cualquier que apunte a producción
  • Un PostToolUse hook registra cada acción de Claude en un audit trail con timestamps, usuario y session ID
  • Un Stop hook verifica que el número de tests passing no haya disminuido antes de entregar el resultado
Resultado: Zero incidentes de acceso a producción en 6 meses de uso intensivo. El audit log proporcionó evidencia para compliance regulatorio.
📢 Notification hook: macOS + Slack en tiempo real
B — Comunidad 2026

Patrón más adoptado en la comunidad para el hook Stop: notificación multi-canal cuando Claude termina un task largo.

#!/bin/bash # Stop hook — notificación cuando Claude termina # macOS notification osascript -e 'display notification "Claude terminó" with title "Claude Code"' 2>/dev/null # Slack webhook (si está configurado) if [ -n "$SLACK_WEBHOOK" ]; then curl -s -X POST "$SLACK_WEBHOOK" \ -H "Content-type: application/json" \ -d '{"text":"✅ Claude Code terminó la tarea"}' fi exit 0

Árbol de decisión

¿Qué necesitas hacer con el hook? │ ├─── BLOQUEAR antes de que ocurra │ └─── PreToolUse (+ PermissionRequest si es permiso) │ ¿Qué tipo de validación? │ ├─── Regex / pattern matching → command hook (bash script) │ ├─── Semántica compleja → prompt hook (haiku) │ └─── Verificación con herramientas → agent hook │ ├─── PROCESAR después de que ocurra │ └─── PostToolUse (no puede bloquear) │ ├─── Auto-format → command hook │ ├─── Audit log → command hook (async) │ └─── Integración externa → http hook │ ├─── INYECTAR contexto al inicio │ └─── SessionStart (matcher: "startup") │ stdout JSON con exit 0 = inyectado a Claude │ ├─── VALIDAR antes de entregar el resultado │ └─── Stop hook │ ├─── Tests pasan → command hook │ └─── Notificación → command hook (async) │ └─── CONTROLAR el agentic loop └─── PostToolBatch exit 2 = para el loop completo

Cuándo NO usar hooks

Un prompt directo resuelve la tarea Latencia del hook supera el beneficio Lógica compleja → usar MCP server UI interactiva requerida Una sola vez → no justifica hook permanente

Honestidad epistémica y fuentes

[A] Verificado — Docs oficiales Anthropic [B] Ampliamente reportado — Múltiples fuentes 2026 [C] Heurística comunitaria