Probablemente no necesitas hooks en Claude Code.
Al menos eso es lo que yo pensaba. Llevo meses usando Claude Code a diario, y todo funcionaba. Le digo qué hacer, lo hace, reviso, sigo.
Pero "funciona" y "funciona bien" son cosas distintas. La diferencia, en mi caso, fue descubrir que las instrucciones que escribía en CLAUDE.md no eran deterministas. Podía escribir "ejecuta vue-tsc después de cada cambio" y Claude lo haría... cuando le pareciese relevante. O cuando el contexto no se hubiese compactado y perdido esa instrucción. O cuando no decidiese que "esta vez no hace falta."
Un hook no negocia. Se ejecuta siempre. Cada vez. Sin excepción.
Esa distinción — instrucción probabilística vs ejecución determinista — es lo que hace que hooks sea una de las funcionalidades más infravaloradas de Claude Code. No aparece en el onboarding, la documentación oficial la presenta como una referencia técnica densa, y la mayoría de usuarios no la descubre hasta que algo se rompe en producción por tercera vez.
Este artículo no es un volcado de referencia. Es la guía que me habría gustado tener: qué hooks instalar, en qué orden, qué tipos existen, qué sale mal, y cuándo NO usarlos.
Anatomía mínima
Un hook es un comando que se ejecuta automáticamente en un punto específico del ciclo de vida de Claude Code. La configuración tiene tres niveles:
- Evento: cuándo se dispara (antes de una herramienta, después, al parar, al compactar...)
- Matcher: regex que filtra si el hook aplica (nombre de herramienta, tipo de evento)
- Handler: el comando o prompt que se ejecuta
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "tu-script.sh",
"timeout": 30
}
]
}
]
}
}
Dónde configurar
| Ubicación | Alcance | Compartible |
|---|---|---|
~/.claude/settings.json |
Todos tus proyectos | No |
.claude/settings.json |
Proyecto actual | Sí (va a git) |
.claude/settings.local.json |
Proyecto actual | No (gitignored) |
También puedes usar /hooks en Claude Code para gestionarlos visualmente sin editar JSON.
Cómo responde tu script
- Exit 0: todo bien, continúa
- Exit 2: error de bloqueo — stderr se envía a Claude como feedback
- Cualquier otro: error no bloqueante, se registra en modo verbose (
Ctrl+O)
Con eso es suficiente. Vamos a lo real.
Los 3 tipos de hooks (mismo caso, 3 enfoques)
Esto es algo que ninguna guía explica bien. Claude Code tiene tres tipos de handlers, no solo scripts de bash. Vamos a ver los tres con el mismo caso de uso: "asegurar que los tests pasan antes de que Claude termine de trabajar."
Command hook (bash script)
El más directo. Un script que ejecuta los tests y bloquea si fallan:
#!/bin/bash
# Bloquea Stop si los tests no pasan
INPUT=$(cat)
# Prevenir bucle infinito — crítico
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
OUTPUT=$(npm test 2>&1)
if [ $? -ne 0 ]; then
echo "Tests fallando. Corrige antes de terminar: $OUTPUT" >&2
exit 2
fi
exit 0
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".claude/hooks/test-gate.sh",
"timeout": 120
}
]
}
]
}
}
Prompt hook (evaluación LLM de un turno)
En lugar de un script, le pides a un modelo que evalúe si Claude debería parar. Útil cuando la decisión no es binaria:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evalúa si Claude debería terminar. Contexto: $ARGUMENTS\n\nVerifica:\n1. Todas las tareas solicitadas están completas\n2. No hay errores pendientes\n3. No queda trabajo de seguimiento\n\nResponde con JSON: {\"ok\": true} para permitir parar, o {\"ok\": false, \"reason\": \"explicación\"} para continuar.",
"timeout": 30
}
]
}
]
}
}
Agent hook (subagente multi-turno con herramientas)
El más potente. Un subagente que puede leer archivos, ejecutar grep, y razonar en múltiples turnos:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verifica que todos los tests unitarios pasan. Ejecuta la suite de tests, lee los resultados, y confirma que no hay fallos. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}
Cuándo usar cada tipo
| Aspecto | Command | Prompt | Agent |
|---|---|---|---|
| Velocidad | Rápido (ms-segundos) | Medio (1-5s) | Lento (5-30s) |
| Coste | Cero | Tokens LLM | Más tokens LLM |
| Flexibilidad | Rígido (exit codes) | Media (juicio simple) | Alta (razonamiento + herramientas) |
| Mejor para | Verificaciones binarias (pass/fail) | Evaluaciones subjetivas | Verificaciones que requieren leer código |
Mi recomendación: empieza con command hooks siempre. Solo sube a prompt o agent cuando necesites juicio, no verificación.
Mis 5 hooks esenciales
Estos son los hooks que instalo en cualquier proyecto. En este orden, porque cada uno resuelve un problema más específico que el anterior.
1. Notificación cuando Claude necesita atención
El hook más simple y más útil. Si trabajas con Claude en segundo plano mientras haces otra cosa, necesitas saber cuándo te necesita:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude necesita tu atención\" with title \"Claude Code\"'"
}
]
}
]
}
}
En Linux, sustituye osascript por notify-send "Claude Code" "Claude necesita tu atención".
2. Bloquear comandos destructivos
Esto debería venir activado por defecto. No lo está:
#!/bin/bash
# Bloquea rm -rf, DROP TABLE, force push y similares
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
BLOCKED_PATTERNS=("rm -rf /" "DROP TABLE" "DROP DATABASE" "git push --force" "git push -f" "> /dev/sda")
for pattern in "${BLOCKED_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "Comando bloqueado por hook de seguridad: contiene '$pattern'" >&2
exit 2
fi
done
exit 0
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-destructive.sh"
}
]
}
]
}
}
Nótese que es PreToolUse, no PostToolUse. Queremos bloquear ANTES de que se ejecute, no después.
3. Auto-format después de cada edición
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null",
"timeout": 15
}
]
}
]
}
}
Siempre exit 0 — no queremos que un problema de formato bloquee a Claude. Si Prettier no puede resolver algo, lo verás en tu IDE. Redirigimos stderr a /dev/null para que Claude no se distraiga.
4. Type-check después de cada edición
#!/bin/bash
# Ejecuta tsc/vue-tsc después de cada edición
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Solo archivos TS/Vue
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.tsx && "$FILE_PATH" != *.vue ]]; then
exit 0
fi
OUTPUT=$(npx vue-tsc --noEmit 2>&1)
if [ $? -ne 0 ]; then
echo "$OUTPUT" >&2
exit 2
fi
exit 0
Este SÍ bloquea (exit 2). Cuando vue-tsc falla, Claude recibe el error y lo corrige antes de seguir. Es la diferencia entre un junior que te entrega código roto y un compañero que compila antes de hacer push.
Adapta vue-tsc a tu stack: tsc para TypeScript puro, tsc --build para monorepos.
5. Re-inyectar contexto después de compactación
Este es el que menos gente conoce y el que más impacto tiene en sesiones largas. Cuando Claude compacta el contexto, puede perder instrucciones de tu CLAUDE.md. Este hook las re-inyecta:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Recordatorio post-compactación: usa pnpm, no npm. Ejecuta pnpm test antes de commitear. Sigue los patrones de /src/composables para nuevos composables.'"
}
]
}
]
}
}
El texto que imprimes por stdout se añade como contexto a Claude. Simple, pero salva sesiones.
Patrones avanzados
Tests asíncronos
Para suites de tests que tardan más de unos segundos, usa async: true para que Claude no se quede esperando:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/run-tests.sh",
"async": true,
"timeout": 120
}
]
}
]
}
}
Claude sigue trabajando. Cuando los tests terminan, el resultado llega como contexto en el siguiente turno. Si fallaron, Claude lo sabe y actúa.
Solo type: "command" soporta async. Los prompt y agent hooks no.
Protección de archivos
Bloquea ediciones a archivos que no deberían tocarse:
#!/bin/bash
# Bloquea ediciones a archivos protegidos
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" "pnpm-lock.yaml" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Bloqueado: $FILE_PATH es un archivo protegido" >&2
exit 2
fi
done
exit 0
Hooks en skills
Si creas skills para Claude Code, puedes asociar hooks al frontmatter YAML:
---
name: deploy-check
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/verify-env.sh"
---
El hook solo se activa mientras la skill está en uso. Cuando termina, desaparece.
Qué sale mal (y cómo arreglarlo)
El bucle infinito del Stop hook
El error más común. Tu Stop hook bloquea a Claude (exit 2), Claude intenta parar de nuevo, el hook lo bloquea otra vez, y así infinitamente.
La solución: siempre comprobar stop_hook_active:
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Deja que Claude pare
fi
Si stop_hook_active es true, significa que Claude ya intentó parar y fue bloqueado. Déjalo ir.
Tu .zshrc contamina el JSON
Si tu shell profile imprime algo al iniciar (welcome messages, actualizaciones de nvm, etc.), ese texto se mezcla con la salida JSON de tus hooks y Claude Code no puede parsearlo.
Solución: envuelve cualquier echo en tu .zshrc o .bashrc con:
if [[ $- == *i* ]]; then
echo "Bienvenido" # Solo en shells interactivos
fi
jq no está instalado
Muchos scripts de hooks usan jq para parsear el JSON de stdin. Si no lo tienes: brew install jq (macOS) o apt install jq (Linux).
El timeout mata el hook silenciosamente
Si tu hook tarda más que el timeout configurado, Claude Code lo mata sin aviso. El comportamiento por defecto es como si el hook no existiera. Ajusta el timeout (en segundos) según la operación.
PermissionRequest no se dispara en modo -p
Si usas claude -p "haz algo" (modo no interactivo), los hooks de PermissionRequest no se ejecutan. Usa PreToolUse en su lugar para decisiones automáticas de permisos.
Framework de decisión
La pregunta que surge siempre: ¿debería usar un hook, una instrucción en CLAUDE.md, un servidor MCP, o una skill?
flowchart TD
Start[¿Qué necesitas?] --> Q1{¿Debe ejecutarse\nSIEMPRE, sin excepción?}
Q1 -->|Sí| Hook[Hook]
Q1 -->|No| Q2{¿Requiere juicio\no interpretación?}
Q2 -->|Sí| CLAUDE[CLAUDE.md]
Q2 -->|No| Q3{¿Necesita conectar\ncon un servicio externo?}
Q3 -->|Sí| MCP[MCP Server]
Q3 -->|No| Q4{¿Es un flujo\nmulti-paso repetible?}
Q4 -->|Sí| Skill[Skill]
Q4 -->|No| CLAUDE
| Aspecto | Hook | CLAUDE.md | MCP | Skill |
|---|---|---|---|---|
| Fiabilidad | Determinista | Probabilística | Determinista | Determinista |
| Flexibilidad | Baja | Alta | Media | Alta |
| Coste de contexto | Cero | Tokens | Tokens | Tokens |
| Compartible en equipo | Sí (.claude/) | Sí | Sí | Sí |
| Requiere código | Sí (bash/json) | No | Sí (servidor) | No (markdown) |
| Mejor para | Verificaciones, formato, seguridad | Guías, patrones, estilo | APIs externas, BD, servicios | Workflows, generación, procesos |
Los 15 eventos de un vistazo
| Evento | Cuándo se dispara | ¿Bloquea? | Caso de uso típico |
|---|---|---|---|
SessionStart |
Al iniciar/reanudar sesión | No | Re-inyectar contexto, cargar variables |
UserPromptSubmit |
Al enviar un prompt | Sí | Validar/transformar entrada |
PreToolUse |
Antes de ejecutar herramienta | Sí | Seguridad, permisos, bloqueos |
PermissionRequest |
Al pedir permiso | Sí | Auto-aprobar/denegar |
PostToolUse |
Después de herramienta exitosa | No* | Formato, lint, type-check |
PostToolUseFailure |
Después de herramienta fallida | No | Logging, contexto adicional |
Notification |
Al notificar | No | Alertas de escritorio |
SubagentStart |
Al crear subagente | No | Logging, contexto |
SubagentStop |
Al terminar subagente | Sí | Validar resultado |
Stop |
Al terminar respuesta | Sí | Quality gates, tests |
TeammateIdle |
Al quedar inactivo un teammate | Sí | Mantener activo |
TaskCompleted |
Al completar tarea | Sí | Verificar calidad |
ConfigChange |
Al cambiar configuración | Sí | Auditoría |
PreCompact |
Antes de compactar contexto | No | Guardar estado |
SessionEnd |
Al terminar sesión | No | Limpieza |
* PostToolUse no puede deshacer la acción, pero el feedback (exit 2) llega a Claude.
Documentación oficial: Hooks Reference · Hooks Guide
Cierre
Los hooks son la infraestructura invisible que convierte a Claude de "probablemente funciona" a "verificado cada vez." No son glamurosos. No son difíciles. Y es tentador ignorarlos.
Pero después de usarlos diariamente, me resulta inconcebible trabajar sin ellos. Empecé con la notificación. Añadí el type-check al tercer archivo roto que nadie detectó a tiempo. El bloqueador de comandos destructivos llegó después de un susto con un rm -rf que no debería haber ocurrido. Y el hook de compactación... ese salvó sesiones enteras.
Cinco hooks. Configuración mínima. Impacto máximo.
Si te interesa cómo extender Claude Code con herramientas externas, la guía de MCP es el complemento natural. Para orquestar tareas complejas, la guía de subagentes. Y si quieres construir flujos de trabajo completos con instrucciones reutilizables, la guía de skills es el siguiente paso.