Módulo 5: Skills y Hooks: automatizar tu workflow

Hooks: Lifecycle Events de Claude Code

Hooks: Lifecycle Events de Claude Code

Descripción

Los Skills te dan slash commands para tareas bajo demanda. Los Hooks hacen algo diferente: ejecutan scripts automáticamente en puntos específicos del ciclo de vida de Claude Code. No tienes que invocarlos — se disparan solos cuando ocurre el evento configurado.

Piensa en los hooks como sensores en una línea de producción. Cuando Claude escribe un archivo, un hook puede ejecutar el linter automáticamente. Cuando Claude ejecuta un comando, un hook puede verificar que no sea destructivo. Cuando Claude completa una tarea, un hook puede ejecutar los tests. Todo sin que tú hagas nada.

Esta cápsula cubre los 5 eventos del ciclo de vida, cómo configurar hooks en settings.json, cómo escribir scripts de hook, cómo usar matchers para filtrar herramientas, y cómo controlar el flujo con return codes.


Qué son los Hooks

Un hook es una asociación entre un evento del ciclo de vida de Claude Code y un script que se ejecuta cuando ese evento ocurre.

┌──────────────────────────────────────────────────────────────┐
│                    CICLO DE VIDA                             │
│                                                              │
│  ┌──────────────┐     ┌──────────────┐                       │
│  │ PreToolUse   │──▶  │ Verificar    │──▶ ¿Proceder?         │
│  └──────────────┘     │ antes de     │    Sí → continúa      │
│         │             │ actuar       │    No → bloquea        │
│         ▼             └──────────────┘                       │
│  ┌──────────────┐                                            │
│  │ [Claude usa  │                                            │
│  │  la tool]    │                                            │
│  └──────────────┘                                            │
│         │                                                    │
│         ▼                                                    │
│  ┌──────────────┐     ┌──────────────┐                       │
│  │ PostToolUse  │──▶  │ Validar      │──▶ Log/formato/test   │
│  └──────────────┘     │ después de   │                       │
│         │             │ actuar       │                       │
│         ▼             └──────────────┘                       │
│  ┌──────────────┐                                            │
│  │Notification  │──▶ Cuando Claude envía notificación        │
│  └──────────────┘                                            │
│         │                                                    │
│         ▼                                                    │
│  ┌──────────────┐                                            │
│  │ Stop         │──▶ Cuando el agente principal se detiene   │
│  └──────────────┘                                            │
│         │                                                    │
│         ▼                                                    │
│  ┌──────────────┐                                            │
│  │SubagentStop  │──▶ Cuando un subagente se detiene          │
│  └──────────────┘                                            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Todos los eventos de hook disponibles

Claude Code expone 22+ eventos organizados por categoría. Esta es la lista completa actualizada a abril 2026:

Session Lifecycle:

EventoCuándo se dispara
SessionStartAl iniciar o resumir una sesión
SessionEndAl terminar una sesión
InstructionsLoadedAl cargar CLAUDE.md o .claude/rules/*.md

Per-Turn Events:

EventoCuándo se dispara
UserPromptSubmitCuando envías un prompt, antes de que Claude lo procese
StopCuando Claude termina de responder
StopFailureCuando el turno termina por error de API

Tool Execution (Agentic Loop):

EventoCuándo se dispara
PreToolUseAntes de ejecutar una tool (puede bloquearla)
PostToolUseDespués de que una tool termina exitosamente
PostToolUseFailureDespués de que una tool falla
PermissionRequestCuando aparece el dialog de permisos
PermissionDeniedCuando el clasificador de auto mode deniega una tool

Agent Team / Subagents:

EventoCuándo se dispara
SubagentStartCuando se lanza un subagent
SubagentStopCuando un subagent termina
TeammateIdleCuando un teammate de agent team va a quedar idle
TaskCreatedCuando se crea una tarea via TaskCreate
TaskCompletedCuando una tarea se marca como completada

File & Configuration Events:

EventoCuándo se dispara
ConfigChangeCuando cambia un archivo de configuración en sesión
CwdChangedCuando cambia el working directory
FileChangedCuando cambia un archivo watched en disco
WorktreeCreateCuando se crea un worktree
WorktreeRemoveCuando se elimina un worktree

Context Management:

EventoCuándo se dispara
PreCompactAntes de la compaction de contexto
PostCompactDespués de completar la compaction

MCP Integration:

EventoCuándo se dispara
ElicitationCuando un MCP server pide input del usuario
ElicitationResultDespués de que el usuario responde

Otros:

EventoCuándo se dispara
NotificationCuando Claude Code envía una notificación

Los 5 eventos que vas a usar más

De todos los eventos anteriores, estos son los que dominan en uso diario. Cubriremos cada uno en profundidad:

EventoCuándo se disparaUso típico
PreToolUseAntes de que Claude use una herramientaGating: bloquear acciones no permitidas
PostToolUseDespués de que Claude usa una herramientaValidación: lint, format, tests
UserPromptSubmitAntes de que Claude procese tu promptInyectar contexto, validar prompts
SessionStartAl iniciar/resumir sesiónCargar contexto inicial, secrets, variables
StopCuando Claude termina de responderTests finales, notificaciones, resúmenes

Los eventos PreToolUse y PostToolUse son los más usados y soportan matcher para filtrar por herramienta. UserPromptSubmit es clave para pipelines de AI — puedes inyectar contexto dinámico. SessionStart sirve para setup (cargar env vars, secrets). Stop permite verificaciones finales.

Eventos nuevos importantes (post-2025):

  • UserPromptSubmit → valida/enriquece prompts antes de procesarse
  • PreCompact / PostCompact → hook cuando el context window se compacta
  • SessionStart / SessionEnd → setup/teardown de sesión
  • InstructionsLoaded → tracking de carga de CLAUDE.md
  • Elicitation → integración con MCP cuando un server pide input

Dónde se configuran

Los hooks se configuran en archivos settings.json de Claude Code:

  • Global: ~/.claude/settings.json (aplica a todos tus proyectos)
  • Proyecto: .claude/settings.json (compartido con el equipo via git)
  • Personal: .claude/settings.local.json (solo tú, en .gitignore)

La configuración usa una clave hooks con los nombres de eventos como sub-claves. Cada hook tiene:

  • type: Tipo de handler (command, http, prompt, o agent)
  • command / url / etc.: Dependiendo del tipo
  • matcher (opcional, para PreToolUse y PostToolUse): Filtra por herramienta específica
  • if (opcional): Condición adicional para ejecutar el hook
  • timeout (opcional): Timeout en segundos
  • statusMessage (opcional): Mensaje que se muestra mientras ejecuta

Los 4 tipos de Hook Handler

Claude Code soporta 4 tipos de handler para hooks:

TipoDescripciónCuándo usar
commandEjecuta un comando shellScripts locales, CLIs, la opción por default
httpPOST request a un endpoint HTTPIntegrar con APIs externas (Slack, Datadog, servicios propios)
promptEvaluación LLM de un solo turnDecisiones que necesitan juicio (es este prompt seguro?)
agentVerificación con un subagentValidaciones complejas que requieren contexto

Ejemplo con handler command (el más común):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/script.sh",
            "if": "Bash(rm *)",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Ejemplo con handler http:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "http",
            "url": "https://my-service.com/claude-done",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Ejemplo con handler prompt (LLM evalúa):

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "¿Este prompt contiene credenciales o datos sensibles? Responde solo 'si' o 'no'."
          }
        ]
      }
    ]
  }
}

Comportamiento importante de los hooks

  • El stdout del hook se muestra a Claude como feedback del usuario. Esto significa que puedes comunicar información a Claude desde tus scripts.
  • En PreToolUse, un hook que retorna un exit code distinto de cero (non-zero) bloquea la ejecución de la herramienta. Esto permite crear gates de seguridad.
  • En otros eventos, un exit code non-zero se reporta como error pero no bloquea la operación.

Visibilidad del output de hooks

  • stdout → Claude lo ve como feedback del usuario. Úsalo para comunicar resultados.
  • stderr → Se registra en logs pero Claude no lo ve directamente.
  • Exit code 0 → Hook exitoso, la operación continúa.
  • Exit code ≠ 0 → Para PreToolUse, BLOQUEA la ejecución del tool. Para PostToolUse, solo se reporta.

Formato de configuración

Cada evento recibe un array de hooks. Ejemplo completo:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/pre-write-check.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/post-write-validate.sh"
      }
    ],
    "Notification": [
      {
        "command": "echo 'Claude envió una notificación'"
      }
    ],
    "Stop": [
      {
        "command": "npm test"
      }
    ],
    "SubagentStop": [
      {
        "command": "echo 'Subagente terminó'"
      }
    ]
  }
}

Tool Matchers

Los matchers filtran qué herramienta debe disparar un hook de PreToolUse o PostToolUse. Sin matcher, el hook se ejecuta para todas las herramientas.

Herramientas disponibles para matching

MatcherQué intercepta
WriteClaude escribe o edita un archivo
ExecuteClaude ejecuta un comando en terminal
ReadClaude lee un archivo
GrepClaude busca en archivos
GlobClaude busca archivos por patrón
WebFetchClaude hace una petición HTTP

Ejemplo: Hook solo para escritura de archivos

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx eslint --fix $CLAUDE_FILE_PATH"
      }
    ]
  }
}

Este hook solo se ejecuta cuando Claude escribe un archivo. No se ejecuta cuando lee archivos, ejecuta comandos, o busca en el codebase.

Múltiples hooks para el mismo evento

Puedes configurar múltiples hooks para un mismo evento:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx prettier --write $CLAUDE_FILE_PATH"
      },
      {
        "matcher": "Write",
        "command": "npx eslint $CLAUDE_FILE_PATH"
      },
      {
        "matcher": "Execute",
        "command": "echo 'Comando ejecutado: $CLAUDE_TOOL_INPUT'"
      }
    ]
  }
}

Los hooks se ejecutan en orden secuencial.


Variables de entorno en Hooks

Cuando un hook se ejecuta, Claude Code pasa información sobre la acción a través de variables de entorno:

VariableDescripciónDisponible en
CLAUDE_FILE_PATHPath del archivo que se está escribiendo/leyendoWrite, Read
CLAUDE_TOOL_INPUTInput completo de la herramienta (JSON)Todas
CLAUDE_TOOL_NAMENombre de la herramienta (Write, Execute, etc.)Todas
CLAUDE_SESSION_IDID de la sesión actualTodas
CLAUDE_PROJECT_DIRDirectorio raíz del proyectoTodas

Ejemplo: Usar variables de entorno

#!/bin/bash
# scripts/post-write-validate.sh

echo "Archivo modificado: $CLAUDE_FILE_PATH"
echo "Herramienta: $CLAUDE_TOOL_NAME"

# Solo ejecutar lint en archivos TypeScript
if [[ "$CLAUDE_FILE_PATH" == *.ts ]] || [[ "$CLAUDE_FILE_PATH" == *.tsx ]]; then
    npx eslint "$CLAUDE_FILE_PATH"
fi

Return codes: controlar el flujo

El return code del script de hook determina qué hace Claude Code después:

Return codeEfecto en PreToolUseEfecto en PostToolUse
0 (éxito)Claude procede con la acciónClaude continúa normalmente
No-0 (error)Claude bloquea la acciónClaude ve el error y puede actuar

Esto es fundamental para PreToolUse: puedes usar hooks para bloquear acciones que no cumplen condiciones.

Ejemplo: Bloquear commits sin tests

#!/bin/bash
# scripts/pre-commit-check.sh

if [[ "$CLAUDE_TOOL_INPUT" == *"git commit"* ]]; then
    # Ejecutar tests antes de permitir commit
    npm test --silent
    if [ $? -ne 0 ]; then
        echo "ERROR: Tests failing. Fix tests before committing."
        exit 1  # Bloquea el commit
    fi
fi

exit 0  # Permite la acción
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Execute",
        "command": "./scripts/pre-commit-check.sh"
      }
    ]
  }
}

Si los tests fallan, el hook retorna 1 y Claude no ejecuta el commit. Claude ve el mensaje de error y puede decidir arreglar los tests primero.


PreToolUse: gating antes de actuar

PreToolUse se ejecuta antes de que Claude use una herramienta. Es tu oportunidad de validar, verificar, o bloquear una acción.

Ejemplo básico: Log de acciones

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "command": "echo 'Claude va a escribir: $CLAUDE_FILE_PATH'"
      }
    ]
  }
}

Ejemplo intermedio: Validar naming convention

#!/bin/bash
# scripts/check-naming.sh

FILE="$CLAUDE_FILE_PATH"

# Verificar que los archivos de componentes usan PascalCase
if [[ "$FILE" == src/components/*.tsx ]]; then
    BASENAME=$(basename "$FILE" .tsx)
    if [[ ! "$BASENAME" =~ ^[A-Z] ]]; then
        echo "ERROR: Componentes deben usar PascalCase. '$BASENAME' no cumple."
        exit 1
    fi
fi

# Verificar que los archivos de test tienen prefijo test_
if [[ "$FILE" == tests/*.py ]]; then
    BASENAME=$(basename "$FILE")
    if [[ ! "$BASENAME" == test_* ]]; then
        echo "ERROR: Tests deben empezar con 'test_'. '$BASENAME' no cumple."
        exit 1
    fi
fi

exit 0

Si Claude intenta crear src/components/userProfile.tsx (minúscula), el hook bloquea la escritura y Claude ve el error. Claude puede entonces corregir el nombre a UserProfile.tsx.

Ejemplo avanzado: Prevenir operaciones peligrosas

#!/bin/bash
# scripts/safety-gate.sh

INPUT="$CLAUDE_TOOL_INPUT"

# Bloquear rm -rf en directorios importantes
if [[ "$INPUT" == *"rm -rf"* ]]; then
    for dir in "src" "lib" "app" "node_modules" ".git"; do
        if [[ "$INPUT" == *"$dir"* ]]; then
            echo "BLOQUEADO: No se permite rm -rf en directorios críticos ($dir)"
            exit 1
        fi
    done
fi

# Bloquear git push --force a main
if [[ "$INPUT" == *"git push"*"--force"* ]] && [[ "$INPUT" == *"main"* ]]; then
    echo "BLOQUEADO: No se permite force push a main"
    exit 1
fi

exit 0

PostToolUse: validar después de actuar

PostToolUse se ejecuta después de que Claude usa una herramienta. Es ideal para validaciones, formateo, y verificaciones automáticas.

Ejemplo básico: Auto-format con Prettier

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx prettier --write $CLAUDE_FILE_PATH 2>/dev/null || true"
      }
    ]
  }
}

Cada vez que Claude escribe un archivo, Prettier lo formatea automáticamente.

Ejemplo intermedio: Lint después de escritura

#!/bin/bash
# scripts/post-write-lint.sh

FILE="$CLAUDE_FILE_PATH"

# Solo lint en archivos JS/TS
case "$FILE" in
    *.js|*.jsx|*.ts|*.tsx)
        echo "Ejecutando ESLint en $FILE..."
        npx eslint "$FILE" 2>&1
        ;;
    *.py)
        echo "Ejecutando ruff en $FILE..."
        ruff check "$FILE" 2>&1
        ;;
    *.css|*.scss)
        echo "Ejecutando Stylelint en $FILE..."
        npx stylelint "$FILE" 2>&1
        ;;
esac

Si el linter encuentra errores, Claude los ve en el output y puede corregirlos automáticamente en su siguiente acción.

Ejemplo avanzado: Tests automáticos después de cambios

#!/bin/bash
# scripts/post-write-test.sh

FILE="$CLAUDE_FILE_PATH"

# Si se modificó un archivo de source, buscar y ejecutar su test
if [[ "$FILE" == src/*.ts ]] || [[ "$FILE" == src/*.tsx ]]; then
    # Construir el path del test
    TEST_FILE="${FILE/src\//tests/test_}"
    TEST_FILE="${TEST_FILE/.tsx/.test.tsx}"
    TEST_FILE="${TEST_FILE/.ts/.test.ts}"
    
    if [ -f "$TEST_FILE" ]; then
        echo "Ejecutando tests relacionados: $TEST_FILE"
        npx vitest run "$TEST_FILE" --reporter=verbose 2>&1
    fi
fi

# Si se modificó un test, ejecutar ese test directamente
if [[ "$FILE" == *test* ]] || [[ "$FILE" == *spec* ]]; then
    echo "Ejecutando test modificado: $FILE"
    npx vitest run "$FILE" --reporter=verbose 2>&1
fi

Stop: verificaciones finales

Stop se ejecuta cuando el agente principal de Claude Code se detiene. Es ideal para verificaciones globales y notificaciones al final de una sesión de trabajo.

Ejemplo básico

{
  "hooks": {
    "Stop": [
      {
        "command": "echo 'Agente detenido. Verificando estado...'"
      }
    ]
  }
}

Ejemplo intermedio: Resumen post-tarea

#!/bin/bash
# scripts/stop-summary.sh

echo "=== Resumen post-tarea ==="
echo "Archivos modificados:"
git diff --name-only 2>/dev/null | head -10
echo ""
echo "Tests:"
npm test --silent 2>/dev/null && echo "Todos passing" || echo "Algunos tests fallando"
echo "========================="

Ejemplo avanzado: Notificación con estado completo

#!/bin/bash
# scripts/on-stop.sh

MODIFIED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')

echo "=== Agente detenido ==="
echo "Archivos modificados: $MODIFIED"
echo "Archivos nuevos: $UNTRACKED"

# Type checking
if [ -f "tsconfig.json" ]; then
    npx tsc --noEmit 2>&1 | tail -1
fi

# Lint
npx eslint src/ --quiet 2>&1 | tail -3

echo "========================="

Notification: alertas personalizadas

Notification se ejecuta cuando Claude envía una notificación. Es útil para logging o alertas personalizadas.

Ejemplo básico

{
  "hooks": {
    "Notification": [
      {
        "command": "echo 'Claude envió una notificación' >> ~/claude-notifications.log"
      }
    ]
  }
}

SubagentStop: validar resultados de subagentes

SubagentStop se ejecuta cuando un subagente (invocado por el agente principal) termina su trabajo. Es útil para validar los resultados parciales de subagentes.

Ejemplo básico

{
  "hooks": {
    "SubagentStop": [
      {
        "command": "echo 'Subagente completó su tarea'"
      }
    ]
  }
}

Comparaciones y decisiones

Hooks vs hacer todo manualmente

AspectoManualCon Hooks
ConsistenciaDepende de recordarSiempre se ejecuta
VelocidadHay que escribir/pegar comandosAutomático
Errores humanosFrecuentes (olvidar lint, tests)Eliminados
Setup inicialNinguno10-15 minutos
Overhead por acciónVariable (según lo que recuerdes)Fijo (el script se ejecuta siempre)

Hooks de Claude Code vs git hooks

AspectoClaude Code HooksGit Hooks
EventosPreToolUse, PostToolUse, Notification, Stop, SubagentStoppre-commit, post-commit, pre-push, etc.
Cuándo se ejecutanDurante la sesión de Claude CodeAl hacer operaciones git
ContextoSaben qué herramienta usó Claude, qué archivo modificóSolo saben que hay un commit/push
Quién los ejecutaClaude Code (el agente)Git (la herramienta)
Son complementarios✅ Sí — validan durante desarrollo✅ Sí — validan al guardar en git

No compiten — se complementan. Los hooks de Claude Code atrapan problemas durante el desarrollo. Los git hooks los atrapan al commitear.

Cuándo usar cada evento

NecesidadEvento
Bloquear una acción antes de que ocurraPreToolUse
Validar el resultado de una acciónPostToolUse
Auto-formatear código después de escribirPostToolUse + matcher Write
Prevenir comandos destructivosPreToolUse + matcher Execute
Ejecutar tests relacionadosPostToolUse + matcher Write
Verificación global al terminarStop
Personalizar alertas o loggingNotification
Validar resultados de subagentesSubagentStop

Patterns comunes

Pattern 1: Pipeline de validación post-escritura

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx prettier --write $CLAUDE_FILE_PATH 2>/dev/null"
      },
      {
        "matcher": "Write",
        "command": "npx eslint $CLAUDE_FILE_PATH 2>/dev/null"
      }
    ]
  }
}

Cada archivo escrito pasa por format → lint automáticamente.

Pattern 2: Safety gate para comandos

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Execute",
        "command": "./scripts/safety-gate.sh"
      }
    ]
  }
}

Todos los comandos pasan por un filtro de seguridad antes de ejecutarse.

Pattern 3: Testing continuo

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/run-related-tests.sh"
      }
    ]
  }
}

Cada vez que Claude escribe código, se ejecutan los tests relacionados.

Pattern 4: Resumen al finalizar

{
  "hooks": {
    "Stop": [
      {
        "command": "./scripts/stop-summary.sh"
      }
    ]
  }
}

Cada vez que Claude termina su trabajo, se ejecuta un script de resumen que muestra el estado final del proyecto.


Pitfalls y edge cases

Pitfall 1: Hooks lentos

El error: Un hook PostToolUse que ejecuta toda la suite de tests después de cada escritura.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npm test"
      }
    ]
  }
}

El problema: Si npm test tarda 30 segundos y Claude escribe 10 archivos, son 5 minutos de espera solo en hooks.

La solución: Ejecuta solo tests relacionados, no toda la suite:

#!/bin/bash
# Solo tests del archivo modificado
npx vitest run --reporter=dot "$CLAUDE_FILE_PATH" 2>/dev/null || true

O mueve la suite completa al hook de Stop (se ejecuta solo una vez cuando Claude termina).

Pitfall 2: Loops infinitos

El error: Un hook PostToolUse que modifica el archivo que Claude acaba de escribir, lo que dispara otro PostToolUse, que modifica el archivo, que dispara otro PostToolUse...

El problema: Loop infinito.

La solución: Claude Code tiene protecciones contra loops, pero diseña tus hooks para que sean idempotentes. Un hook de format (Prettier) es seguro porque formatear un archivo ya formateado no produce cambios.

Prevención de loops infinitos

¿Qué pasa si un PostToolUse hook modifica un archivo? ¿Eso dispara otro PostToolUse?

Respuesta: Claude Code tiene protección integrada contra loops. Los hooks no disparan otros hooks recursivamente. Si tu PostToolUse hook usa el Write tool internamente, esa escritura NO dispara otro ciclo de PostToolUse.

Buena práctica: Diseña tus hooks como operaciones idempotentes — que produzcan el mismo resultado sin importar cuántas veces se ejecuten.

Pitfall 3: Hooks que bloquean operaciones legítimas

El error: Un PreToolUse que bloquea rm sin considerar contexto.

if [[ "$INPUT" == *"rm"* ]]; then
    exit 1  # Bloquea TODO lo que tenga "rm"
fi

El problema: Bloquea operaciones legítimas como npm run format (que puede contener "rm" en su internals).

La solución: Sé específico en los patterns que bloqueas:

if [[ "$INPUT" =~ ^rm\ -rf\ / ]]; then
    exit 1  # Solo bloquea rm -rf en la raíz del sistema
fi

Pitfall 4: Scripts sin permisos de ejecución

El error: Crear un script de hook y olvidar hacerlo ejecutable.

La solución:

chmod +x scripts/post-write-validate.sh

Pitfall 5: Hooks que no manejan errores

El error: Un hook que falla con un error no manejado y bloquea toda la sesión.

La solución: Usa || true para hooks que no deben bloquear:

{
  "command": "npx eslint $CLAUDE_FILE_PATH 2>/dev/null || true"
}

El || true asegura que el hook siempre retorna 0 (éxito), incluso si el linter falla. Usa esto para hooks informativos (donde quieres ver el error pero no bloquear).


Ejemplo completo integrado

Escenario: configuras un proyecto TypeScript + React con hooks de calidad completos.

Estructura de scripts

scripts/
├── hooks/
│   ├── pre-write-check.sh
│   ├── post-write-validate.sh
│   └── on-stop.sh

settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/pre-write-check.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/post-write-validate.sh"
      }
    ],
    "Stop": [
      {
        "command": "./scripts/hooks/on-stop.sh"
      }
    ]
  }
}

Scripts

pre-write-check.sh:

#!/bin/bash
FILE="$CLAUDE_FILE_PATH"

# Verificar que no se escriba en node_modules
if [[ "$FILE" == *node_modules* ]]; then
    echo "BLOQUEADO: No modificar archivos en node_modules"
    exit 1
fi

# Verificar naming de componentes
if [[ "$FILE" == src/components/*.tsx ]]; then
    BASENAME=$(basename "$FILE" .tsx)
    if [[ ! "$BASENAME" =~ ^[A-Z] ]]; then
        echo "BLOQUEADO: Componentes deben usar PascalCase ($BASENAME)"
        exit 1
    fi
fi

exit 0

post-write-validate.sh:

#!/bin/bash
FILE="$CLAUDE_FILE_PATH"

case "$FILE" in
    *.ts|*.tsx|*.js|*.jsx)
        npx prettier --write "$FILE" 2>/dev/null
        npx eslint "$FILE" --quiet 2>/dev/null || true
        ;;
    *.css|*.scss)
        npx prettier --write "$FILE" 2>/dev/null
        ;;
esac

on-stop.sh:

#!/bin/bash
echo "=== Verificación al finalizar ==="
npx tsc --noEmit 2>&1 | tail -3
npm test --silent 2>&1 | tail -3
echo "================================="

Ejercicios prácticos

Ejercicio 1: Básico — Tu primer hook PostToolUse

Configura un hook PostToolUse que muestre un log cada vez que Claude escribe un archivo.

Requisitos:

  • Solo se activa cuando Claude usa la herramienta Write
  • Muestra el path del archivo escrito
  • No debe bloquear la operación
Solución

Agrega a .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "echo \"Archivo escrito: $CLAUDE_FILE_PATH\""
      }
    ]
  }
}

Ejercicio 2: Básico — Hook PostToolUse para formateo

Configura un hook que ejecute Prettier automáticamente después de cada escritura.

Requisitos:

  • Solo en archivos JS/TS/CSS
  • No debe bloquear si Prettier falla
  • Debe ser silencioso (no mostrar output de Prettier si no hay cambios)
Solución

Crea scripts/hooks/auto-format.sh:

#!/bin/bash
FILE="$CLAUDE_FILE_PATH"

case "$FILE" in
    *.js|*.jsx|*.ts|*.tsx|*.css|*.scss|*.json)
        npx prettier --write "$FILE" --log-level=warn 2>/dev/null || true
        ;;
esac
chmod +x scripts/hooks/auto-format.sh

En .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/auto-format.sh"
      }
    ]
  }
}

Ejercicio 3: Intermedio — PreToolUse safety gate

Crea un hook que bloquee comandos peligrosos antes de que Claude los ejecute.

Requisitos:

  • Bloquear rm -rf en directorios como src/, .git/, node_modules/
  • Bloquear git push --force a main o master
  • Permitir todo lo demás
  • Mostrar un mensaje claro cuando se bloquea
Solución

Crea scripts/hooks/safety-gate.sh:

#!/bin/bash
INPUT="$CLAUDE_TOOL_INPUT"

# Bloquear rm -rf en directorios críticos
if [[ "$INPUT" == *"rm -rf"* ]]; then
    for dir in "src" ".git" "node_modules" "lib" "app"; do
        if [[ "$INPUT" == *"$dir"* ]]; then
            echo "⛔ BLOQUEADO: rm -rf en directorio crítico ($dir)"
            exit 1
        fi
    done
fi

# Bloquear force push a main/master
if [[ "$INPUT" == *"git push"*"--force"* ]]; then
    if [[ "$INPUT" == *"main"* ]] || [[ "$INPUT" == *"master"* ]]; then
        echo "⛔ BLOQUEADO: force push a main/master no permitido"
        exit 1
    fi
fi

exit 0
chmod +x scripts/hooks/safety-gate.sh

En .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Execute",
        "command": "./scripts/hooks/safety-gate.sh"
      }
    ]
  }
}

Ejercicio 4: Intermedio — Pipeline post-escritura completo

Configura un pipeline que ejecute format → lint → type-check después de cada escritura de archivo TypeScript.

Requisitos:

  • Prettier para formateo
  • ESLint para linting
  • TypeScript compiler (tsc) para type checking
  • Solo en archivos .ts y .tsx
  • Si lint o type-check fallan, Claude debe ver los errores (pero no bloquear)
Solución

Crea scripts/hooks/post-write-pipeline.sh:

#!/bin/bash
FILE="$CLAUDE_FILE_PATH"

# Solo procesar archivos TypeScript
case "$FILE" in
    *.ts|*.tsx)
        echo "--- Pipeline de validación ---"
        
        # Paso 1: Format
        npx prettier --write "$FILE" --log-level=warn 2>/dev/null
        
        # Paso 2: Lint
        LINT_OUTPUT=$(npx eslint "$FILE" --quiet 2>&1)
        if [ -n "$LINT_OUTPUT" ]; then
            echo "⚠️  ESLint:"
            echo "$LINT_OUTPUT"
        fi
        
        # Paso 3: Type check
        TSC_OUTPUT=$(npx tsc --noEmit 2>&1 | grep "$FILE")
        if [ -n "$TSC_OUTPUT" ]; then
            echo "⚠️  TypeScript:"
            echo "$TSC_OUTPUT"
        fi
        
        echo "--- Pipeline completado ---"
        ;;
esac

exit 0
chmod +x scripts/hooks/post-write-pipeline.sh

Ejercicio 5: Avanzado — Sistema de hooks completo

Configura los 5 eventos de hooks para un proyecto real, con scripts para cada uno.

Requisitos:

  • PreToolUse (Write): validar naming conventions
  • PreToolUse (Execute): safety gate para comandos peligrosos
  • PostToolUse (Write): format + lint
  • Stop: ejecutar tests + resumen
  • Notification: logging de notificaciones
Solución

Usa la configuración del ejemplo completo integrado de esta cápsula como base. Adapta los scripts a las convenciones de tu proyecto (lenguaje, framework, herramientas de lint/test).

Verifica que:

  1. Cada script es ejecutable (chmod +x)
  2. Los hooks se ejecutan correctamente (prueba cada uno)
  3. Los PreToolUse no bloquean operaciones legítimas
  4. Los PostToolUse no son demasiado lentos
  5. El hook de Stop te da información útil al finalizar

Resumen

Lo que aprendiste en esta cápsula:

  • Hooks son scripts que se ejecutan automáticamente en eventos del ciclo de vida de Claude Code
  • 5 eventos más comunes: PreToolUse, PostToolUse, Notification, Stop, SubagentStop (Claude Code expone 20+ eventos en total — ver docs oficiales)
  • Configuración en settings.json (global ~/.claude/settings.json, proyecto .claude/settings.json, o personal .claude/settings.local.json)
  • Matchers filtran qué herramienta dispara el hook (Write, Execute, Read, etc.) -- solo para PreToolUse y PostToolUse
  • Return codes: 0 = proceder, no-0 = bloquear (en PreToolUse)
  • stdout del hook se muestra a Claude como feedback del usuario
  • Variables de entorno: CLAUDE_FILE_PATH, CLAUDE_TOOL_INPUT, CLAUDE_TOOL_NAME dan contexto al hook
  • Pitfalls criticos: hooks lentos, loops infinitos, bloqueos excesivos, scripts sin permisos

Siguiente capsula: 04 - Automatizar validaciones -- como construir un pipeline de calidad completo usando hooks.


Recursos adicionales

Documentación oficial

  • Hooks — Documentación completa de hooks y lifecycle events
  • Settings — Dónde configurar hooks (global, project, local)
  • Plugins Reference — Hooks como parte del sistema de plugins

Complementarios