5Modulo 5

Hooks y Automatizacion

45 min

Objetivo

Comprender qué son los hooks en Claude Code, cómo funcionan y cuándo utilizarlos para automatizar acciones en tu flujo de trabajo.


Introducción

Los hooks son scripts que se ejecutan automáticamente en momentos específicos cuando trabajas con Claude Code. Piensa en ellos como "respuestas automáticas" a ciertos eventos que ocurren durante tu sesión.

Definición formal:

Los hooks son comandos personalizados que se ejecutan de forma determinística en puntos específicos del ciclo de vida de Claude Code, sin necesidad de invocación manual.

En términos simples: son acciones que suceden automáticamente cuando Claude hace algo, sin que tú o Claude tengan que pedirlo explícitamente.


¿Por qué usar hooks?

Imagina este escenario común: cada vez que Claude modifica un archivo TypeScript, tú quieres que se formatee automáticamente con Prettier. Sin hooks, tendrías que:

  1. Esperar a que Claude termine la edición
  2. Recordar ejecutar npx prettier --write archivo.ts
  3. Repetir esto para cada archivo modificado

Con hooks, esto sucede automáticamente. Claude modifica el archivo → el hook ejecuta Prettier → el código queda formateado sin intervención manual.

Beneficios clave

  • Automatización: Elimina tareas repetitivas
  • Consistencia: Las acciones siempre se ejecutan, sin olvidos
  • Eficiencia: Ahorra tiempo en cada sesión
  • Calidad: Asegura que ciertas validaciones o procesos siempre ocurran

Concepto clave: Eventos y acciones

Los hooks funcionan con un patrón simple:

EVENTO → ACCIÓN AUTOMÁTICA

Ejemplo 1: Formateo automático

Evento: Claude edita un archivo .ts
Acción: Ejecutar prettier automáticamente

Ejemplo 2: Tests automáticos

Evento: Claude modifica código en /src
Acción: Ejecutar tests relacionados con ese archivo

Ejemplo 3: Logging de comandos

Evento: Claude ejecuta un comando bash
Acción: Registrar el comando en un archivo de log

Ejemplo 4: Notificaciones

Evento: Claude termina de responder
Acción: Enviar notificación de escritorio

Tipos de eventos disponibles

Claude Code detecta automáticamente estos eventos durante tu sesión:

EventoCuándo ocurre
SessionStartAl iniciar o reanudar una sesión
UserPromptSubmitCuando envías un mensaje a Claude
PreToolUseAntes de que Claude use una herramienta
PostToolUseDespués de que Claude use una herramienta exitosamente
StopCuando Claude termina de responder
SessionEndAl finalizar la sesión

Nota: En el punto 5.2 veremos todos los tipos de eventos en detalle.


¿Qué tipo de acciones puedes automatizar?

Los hooks pueden ejecutar cualquier comando de shell. Casos de uso comunes:

Calidad de código

  • Formatear con Prettier, Black, etc.
  • Ejecutar linters (ESLint, Pylint)
  • Validar sintaxis

Testing

  • Ejecutar tests unitarios relacionados
  • Ejecutar tests de integración
  • Verificar cobertura de código

Seguridad

  • Validar que no se modifiquen archivos sensibles
  • Escanear en busca de secretos hardcodeados
  • Bloquear comandos peligrosos

Productividad

  • Registrar comandos ejecutados (auditoría)
  • Enviar notificaciones
  • Auto-commit de cambios
  • Sincronizar con herramientas externas

Hooks vs. Skills vs. Comandos manuales

Es importante entender la diferencia entre estos tres conceptos:

CaracterísticaHooksSkillsComandos manuales
InvocaciónAutomática (por evento)Manual con /nombreManual (escribes el comando)
Cuándo usarAutomatizar tareas repetitivasEncapsular flujos complejosTareas puntuales
Configuraciónsettings.json.claude/skills/N/A
Puede bloquear accionesNoNo

Ejemplo comparativo

Tarea: Formatear código TypeScript

Opción 1 - Comando manual:

> Ejecuta prettier en src/utils.ts
  • Debes pedirlo cada vez
  • Fácil de olvidar

Opción 2 - Skill:

> /format src/utils.ts
  • Más rápido que escribir el comando completo
  • Aún requiere invocación manual

Opción 3 - Hook:

  • Se ejecuta automáticamente cada vez que Claude edita un .ts
  • Nunca lo olvidas
  • Cero intervención manual

¿Cuándo usar hooks?

Usa hooks cuando

  • La acción debe ocurrir siempre tras un evento específico
  • Quieres automatizar tareas repetitivas
  • Necesitas garantizar que ciertas validaciones siempre se ejecuten
  • Quieres mantener estándares de calidad sin intervención manual

No uses hooks cuando

  • La acción es opcional o contextual
  • Solo necesitas ejecutar algo una vez
  • La acción es compleja y requiere decisiones del LLM
  • Puede ralentizar significativamente el flujo de trabajo

Regla general

Si te encuentras pidiendo a Claude que haga lo mismo repetidamente, probablemente deberías usar un hook.


Ejemplo práctico: El flujo completo

Imagina que estás desarrollando una aplicación en Python. Quieres que:

  1. Todo el código se formatee con Black automáticamente
  2. Se ordenen los imports con isort
  3. Se ejecuten los tests relacionados

Sin hooks:

Tú: Modifica la función calculate_total en src/utils.py
Claude: [Modifica el archivo]
Tú: Ejecuta black en ese archivo
Claude: [Ejecuta black]
Tú: Ahora ejecuta isort
Claude: [Ejecuta isort]
Tú: Ejecuta los tests relacionados
Claude: [Ejecuta tests]

Con hooks:

Tú: Modifica la función calculate_total en src/utils.py
Claude: [Modifica el archivo]
→ Hook 1: Ejecuta black automáticamente
→ Hook 2: Ejecuta isort automáticamente
→ Hook 3: Ejecuta tests automáticamente

Resultado: Lo que antes requería 4 mensajes, ahora sucede con 1.


Vista previa: Lo que aprenderás próximamente

En los siguientes puntos del módulo profundizaremos en:

5.2 Tipos de hooks

  • Todos los eventos disponibles
  • Cuándo se dispara cada uno
  • Variables disponibles en cada evento

5.3 Configuración de hooks

  • Cómo configurar hooks en settings.json
  • Matchers para filtrar eventos
  • Variables de entorno disponibles
  • Códigos de salida y control de flujo

5.4 Casos de uso avanzados

  • Auto-commit después de cambios
  • Ejecutar tests relacionados
  • Notificaciones de sistema
  • Integración con herramientas externas

5.5 Timeout de hooks

  • Configuración de timeouts
  • Manejo de hooks lentos
  • Mejores prácticas de rendimiento

Puntos clave

  1. Los hooks son scripts que se ejecutan automáticamente en eventos específicos
  2. Siguen el patrón EVENTO → ACCIÓN AUTOMÁTICA
  3. Se usan para automatizar tareas repetitivas y garantizar consistencia
  4. Son determinísticos: siempre se ejecutan, sin depender del LLM
  5. Se configuran en settings.json (global o por proyecto)
  6. Diferentes de skills (manuales) y comandos (puntuales)

Recursos adicionales

  • Documentación oficial de hooks
  • Referencia completa de hooks
  • Configuración de settings.json

Próximo paso: 5.2 Tipos de hooks - Conoce todos los eventos disponibles y cuándo usar cada uno.


5.2 Tipos de hooks

Objetivo

Conocer todos los tipos de hooks disponibles en Claude Code, cuándo se disparan, qué información proporcionan y cómo elegir el hook adecuado para cada necesidad.


Introducción

Claude Code ofrece 13 tipos de hooks que cubren todo el ciclo de vida de una sesión. Cada hook se dispara en un momento específico y proporciona información contextual diferente.

En este punto veremos todos los tipos disponibles, organizados en categorías lógicas para facilitar su comprensión.


Vista general: Todos los hooks disponibles

HookCuándo se ejecutaCategoría
SessionStartAl iniciar o reanudar una sesiónCiclo de sesión
SessionEndAl finalizar la sesiónCiclo de sesión
SetupCon flags --init, --init-only o --maintenanceCiclo de sesión
UserPromptSubmitCuando el usuario envía un mensajeInteracción
StopCuando Claude termina de responderInteracción
PreToolUseAntes de ejecutar una herramientaHerramientas
PostToolUseDespués de ejecutar una herramienta (éxito)Herramientas
PostToolUseFailureDespués de ejecutar una herramienta (fallo)Herramientas
PermissionRequestCuando aparece un diálogo de permisoHerramientas
SubagentStartCuando se inicia un subagenteSubagentes
SubagentStopCuando termina un subagenteSubagentes
PreCompactAntes de compactar el contextoEspeciales
NotificationCuando Claude Code envía notificacionesEspeciales

Categoría 1: Hooks de ciclo de sesión

Estos hooks se ejecutan al inicio, durante el setup y al final de una sesión.

SessionStart

Cuándo se ejecuta: Al iniciar una nueva sesión o al reanudar una sesión existente.

Casos de uso:

  • Inicializar variables de entorno específicas de la sesión
  • Cargar configuración dinámica
  • Registrar el inicio de sesión (auditoría)
  • Verificar dependencias necesarias

Variables especiales:

  • $CLAUDE_ENV_FILE: Ruta al archivo donde puedes persistir variables de entorno para la sesión

Ejemplo práctico:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"SESSION_ID=$(date +%s)\" > \"$CLAUDE_ENV_FILE\""
          }
        ]
      }
    ]
  }
}

SessionEnd

Cuándo se ejecuta: Al finalizar la sesión (cierre normal).

Casos de uso:

  • Limpiar archivos temporales
  • Guardar logs de la sesión
  • Enviar reportes o estadísticas
  • Restaurar configuración original

Ejemplo práctico:

{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Sesión finalizada: $(date)\" >> ~/.claude/session-log.txt"
          }
        ]
      }
    ]
  }
}

Setup

Cuándo se ejecuta: Cuando se usa alguno de estos flags:

  • claude --init (inicializar proyecto)
  • claude --init-only (solo configuración inicial)
  • claude --maintenance (mantenimiento)

Casos de uso:

  • Configuración inicial de proyecto
  • Instalación de dependencias
  • Generación de archivos de configuración
  • Verificación de herramientas necesarias

Ejemplo práctico:

{
  "hooks": {
    "Setup": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "npm install && git config --local core.hooksPath .claude/git-hooks"
          }
        ]
      }
    ]
  }
}

Categoría 2: Hooks de interacción

Estos hooks se ejecutan durante la interacción entre tú y Claude.

UserPromptSubmit

Cuándo se ejecuta: Inmediatamente después de que envías un mensaje a Claude, antes de que Claude lo procese.

Casos de uso:

  • Validar que tu prompt cumple ciertas reglas
  • Añadir contexto adicional automáticamente
  • Registrar todos los prompts (auditoría)
  • Bloquear prompts que contengan información sensible

Características especiales:

  • Puede bloquear el prompt (impedir que llegue a Claude)
  • El output del hook se añade al contexto de Claude
  • Útil para inyectar información dinámica

Ejemplo práctico:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Contexto adicional: Branch actual = $(git branch --show-current)\""
          }
        ]
      }
    ]
  }
}

Bloqueando un prompt:

#!/usr/bin/env bash
# .claude/hooks/validate-prompt.sh
 
# Lee el prompt del usuario
prompt=$(jq -r '.user_prompt' | tr '[:upper:]' '[:lower:]')
 
# Bloquea si contiene palabras sensibles
if echo "$prompt" | grep -qE '(password|secret|api[_-]?key)'; then
  echo '{"decision": "block", "reason": "El prompt contiene información sensible"}'
  exit 0
fi

Stop

Cuándo se ejecuta: Cuando Claude termina de responder completamente.

Casos de uso:

  • Enviar notificación de que Claude terminó
  • Validar que Claude completó todas las tareas
  • Decidir si Claude debe continuar trabajando
  • Registrar el final de una interacción

Características especiales:

  • Puede bloquear para que Claude continúe trabajando automáticamente
  • Soporta hooks de tipo prompt (usa Haiku para decidir)

Ejemplo práctico - Notificación:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude ha terminado de responder'"
          }
        ]
      }
    ]
  }
}

Ejemplo avanzado - Continuar automáticamente:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evalúa si Claude debe continuar trabajando basándote en:\n$ARGUMENTS\n\nResponde JSON: {\"ok\": true} para continuar o {\"ok\": false, \"reason\": \"explicación\"} para detener",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Categoría 3: Hooks de herramientas

Estos hooks se disparan cuando Claude usa herramientas (Read, Write, Edit, Bash, etc.).

PreToolUse

Cuándo se ejecuta: Justo antes de que Claude ejecute una herramienta.

Casos de uso:

  • Validar que el comando/operación es segura
  • Bloquear operaciones sobre archivos sensibles
  • Registrar qué herramientas está usando Claude (auditoría)
  • Modificar parámetros de la herramienta antes de ejecutarla
  • Pre-validación de permisos adicional

Características especiales:

  • Puede bloquear la ejecución de la herramienta
  • Puede modificar los parámetros de entrada
  • Requiere configuración de matcher para filtrar herramientas

Herramientas disponibles para matchers:

  • Read, Write, Edit, Bash, Glob, Grep, Task
  • WebFetch, WebSearch
  • mcp__<server>__<tool> (herramientas MCP)

Ejemplo práctico - Bloquear archivos sensibles:

#!/usr/bin/env bash
# .claude/hooks/block-sensitive.sh
 
file_path=$(jq -r '.tool_input.file_path // empty')
 
# Lista de patterns prohibidos
if echo "$file_path" | grep -qE '(\.env|\.git/|node_modules/|\.ssh/)'; then
  # Exit 2 = Bloquear operación
  echo "Acceso denegado a archivo sensible: $file_path" >&2
  exit 2
fi
 
exit 0

Configuración:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-sensitive.sh"
          }
        ]
      }
    ]
  }
}

Ejemplo - Logging de comandos bash:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-commands.log"
          }
        ]
      }
    ]
  }
}

PostToolUse

Cuándo se ejecuta: Justo después de que Claude ejecute una herramienta exitosamente.

Casos de uso:

  • Formatear código automáticamente después de ediciones
  • Ejecutar linters después de modificar archivos
  • Ejecutar tests relacionados
  • Sincronizar con herramientas externas
  • Registrar operaciones completadas

Características especiales:

  • Solo se ejecuta si la herramienta tuvo éxito (exit 0)
  • Requiere configuración de matcher
  • El más usado para automatización de calidad de código

Ejemplo práctico - Formateo automático con Prettier:

#!/usr/bin/env bash
# .claude/hooks/auto-format.sh
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo formatear archivos TypeScript/JavaScript
if echo "$file_path" | grep -qE '\.(ts|tsx|js|jsx)$'; then
  npx prettier --write "$file_path" 2>/dev/null
fi

Configuración:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/auto-format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Ejemplo - Tests automáticos:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "file=$(jq -r '.tool_input.file_path'); npm test -- --findRelatedTests \"$file\" --passWithNoTests",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

PostToolUseFailure

Cuándo se ejecuta: Cuando una herramienta falla durante su ejecución.

Casos de uso:

  • Registrar errores para diagnóstico
  • Enviar alertas de fallos
  • Ejecutar acciones de recuperación
  • Rollback de cambios parciales

Ejemplo práctico:

{
  "hooks": {
    "PostToolUseFailure": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[$(date)] Comando falló\" >> ~/.claude/errors.log && jq '.' >> ~/.claude/errors.log"
          }
        ]
      }
    ]
  }
}

PermissionRequest

Cuándo se ejecuta: Cuando Claude necesita pedir permiso al usuario para ejecutar una herramienta.

Casos de uso:

  • Pre-aprobar ciertos comandos automáticamente
  • Registrar solicitudes de permiso
  • Añadir validaciones adicionales antes del diálogo

Características especiales:

  • Puede auto-aprobar o auto-denegar permisos
  • Útil para automatizar flujos en entornos confiables

Ejemplo práctico:

#!/usr/bin/env bash
# Auto-aprobar comandos git read-only
 
command=$(jq -r '.tool_input.command // empty')
 
# Auto-aprobar comandos git de solo lectura
if echo "$command" | grep -qE '^git (status|log|diff|show|branch)'; then
  echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "permissionDecision": "allow"}}'
  exit 0
fi
 
# Para todo lo demás, preguntar al usuario
echo '{"hookSpecificOutput": {"hookEventName": "PermissionRequest", "permissionDecision": "ask"}}'
exit 0

Categoría 4: Hooks de subagentes

Estos hooks se ejecutan cuando se trabaja con subagentes (instancias especializadas de Claude).

SubagentStart

Cuándo se ejecuta: Cuando Claude inicia un subagente especializado.

Casos de uso:

  • Registrar inicio de subagentes para auditoría
  • Cargar configuración específica para el subagente
  • Notificar que comenzó una tarea en background

Ejemplo práctico:

{
  "hooks": {
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[$(date)] Subagente iniciado\" >> ~/.claude/subagents.log"
          }
        ]
      }
    ]
  }
}

SubagentStop

Cuándo se ejecuta: Cuando un subagente termina su trabajo.

Casos de uso:

  • Notificar que el subagente terminó
  • Validar resultados del subagente
  • Decidir si el subagente debe continuar

Características especiales:

  • Puede bloquear para que el subagente continúe
  • Soporta hooks de tipo prompt

Ejemplo práctico:

{
  "hooks": {
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Subagente completado' 'El subagente ha terminado su tarea'"
          }
        ]
      }
    ]
  }
}

Categoría 5: Hooks especiales

PreCompact

Cuándo se ejecuta: Justo antes de que Claude compacte el contexto de la conversación (comando /compact).

Casos de uso:

  • Guardar snapshot del contexto completo antes de compactar
  • Registrar estadísticas de la sesión
  • Exportar información importante antes de la compactación

Ejemplo práctico:

{
  "hooks": {
    "PreCompact": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Compactación iniciada: $(date)\" >> ~/.claude/compact-log.txt"
          }
        ]
      }
    ]
  }
}

Notification

Cuándo se ejecuta: Cuando Claude Code envía notificaciones del sistema.

Casos de uso:

  • Integrar con sistemas de notificaciones personalizados
  • Filtrar o modificar notificaciones
  • Registrar todas las notificaciones

Ejemplo práctico:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.message' | xargs -I {} notify-send 'Claude Code' '{}'"
          }
        ]
      }
    ]
  }
}

Guía de selección: ¿Qué hook usar?

Necesitas...Usa este hook
Formatear código después de editarPostToolUse con matcher Write|Edit
Ejecutar tests automáticamentePostToolUse con matcher Write|Edit
Bloquear archivos sensiblesPreToolUse con matcher Read|Write|Edit
Registrar comandos bashPreToolUse con matcher Bash
Notificar cuando Claude termineStop
Inicializar variables de sesiónSessionStart
Validar prompts del usuarioUserPromptSubmit
Limpiar al finalizarSessionEnd
Auto-aprobar comandos git segurosPermissionRequest con matcher Bash
Configuración inicial de proyectoSetup
Registrar errores de herramientasPostToolUseFailure
Notificar fin de subagenteSubagentStop

Matchers: Filtrado de herramientas

Para los hooks PreToolUse, PostToolUse, PostToolUseFailure y PermissionRequest, debes especificar qué herramientas disparan el hook usando matchers.

Sintaxis de matchers

{
  "matcher": "PATRÓN"
}

Patrones válidos:

  • "Bash" → Solo herramienta Bash
  • "Write" → Solo herramienta Write
  • "Write|Edit" → Write o Edit (regex con pipe)
  • "*" o "" → Todas las herramientas
  • "mcp__github__*" → Todas las herramientas del servidor MCP de GitHub

Ejemplos de matchers

// Solo para archivos Python
{
  "matcher": "Write|Edit",
  "hooks": [/* ... */]
}
 
// Solo comandos bash
{
  "matcher": "Bash",
  "hooks": [/* ... */]
}
 
// Todas las herramientas
{
  "matcher": "*",
  "hooks": [/* ... */]
}

Resumen visual: Línea de tiempo de hooks

claude (sesión inicia)

  ├─→ SessionStart

  ├─→ UserPromptSubmit ──→ [Usuario envía mensaje]

  ├─→ PreToolUse ──────────→ [Claude va a usar herramienta]

  ├─→ PostToolUse ─────────→ [Herramienta ejecutada con éxito]
  │   o
  ├─→ PostToolUseFailure ──→ [Herramienta falló]

  ├─→ Stop ────────────────→ [Claude terminó de responder]

  ├─→ /compact (manual)
  │   └─→ PreCompact

  └─→ SessionEnd (al cerrar)

Puntos clave

  1. Claude Code ofrece 13 tipos de hooks que cubren todo el ciclo de vida
  2. Los hooks se clasifican en: sesión, interacción, herramientas, subagentes y especiales
  3. Los hooks más usados son: PostToolUse (automatización) y PreToolUse (validación)
  4. Los hooks PreToolUse, PostToolUse, PermissionRequest requieren matchers
  5. Los hooks pueden bloquear operaciones con exit code 2
  6. Cada hook proporciona información contextual específica vía stdin JSON
  7. Variables de entorno como $CLAUDE_PROJECT_DIR están disponibles en todos los hooks

Recursos adicionales


Próximo paso: 5.3 Configuración de hooks - Aprende a configurar hooks en settings.json con ejemplos prácticos.


5.3 Configuración de hooks

Objetivo

Aprender a configurar hooks en settings.json, entender las variables disponibles, códigos de salida, timeouts y mejores prácticas para crear configuraciones robustas y mantenibles.


Introducción

Los hooks se configuran en archivos settings.json usando una estructura JSON específica. En este punto aprenderás todo lo necesario para crear configuraciones efectivas, desde lo más básico hasta patrones avanzados.


Dónde configurar hooks

Claude Code sigue una jerarquía de configuración:

1. ~/.claude/settings.json           → Configuración GLOBAL (todos los proyectos)
2. .claude/settings.json             → Configuración del PROYECTO (en git)
3. .claude/settings.local.json       → Configuración LOCAL (no commitida)

Prioridad: Los archivos más específicos sobrescriben a los más generales.

¿Dónde colocar cada hook?

HookRecomendaciónMotivo
Formateo de códigoProyecto (.claude/settings.json)Todo el equipo se beneficia
Logging personalGlobal (~/.claude/settings.json)Es preferencia personal
API keys o tokensLocal (.claude/settings.local.json)No debe estar en git
Tests automáticosProyectoGarantiza calidad para todos
NotificacionesGlobalPreferencia personal

Estructura básica

La configuración de hooks va dentro de la sección hooks de settings.json:

{
  "hooks": {
    "TIPO_DE_HOOK": [
      {
        "matcher": "PATRÓN",  // Solo para PreToolUse, PostToolUse, PermissionRequest
        "hooks": [
          {
            "type": "command",
            "command": "comando a ejecutar",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Campos principales

CampoDescripciónRequerido
typeTipo de hook: "command" o "prompt"
commandComando shell a ejecutarSí (si type=command)
promptPrompt para evaluación con LLMSí (si type=prompt)
timeoutTimeout en segundosNo (default: 600s)
matcherPatrón para filtrar herramientasSolo en PreToolUse, PostToolUse, etc.

Anatomía de un hook

Veamos paso a paso cómo crear un hook completo.

Ejemplo 1: Hook simple (inline)

Formatear archivos TypeScript después de editarlos:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | { read f; [[ \"$f\" =~ \\.tsx?$ ]] && npx prettier --write \"$f\"; }",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Desglose:

  • "PostToolUse": Se ejecuta después de usar una herramienta
  • "matcher": "Write|Edit": Solo cuando se use Write o Edit
  • jq -r '.tool_input.file_path': Extrae la ruta del archivo del JSON de entrada
  • [[ "$f" =~ \.tsx?$ ]]: Verifica que sea .ts o .tsx
  • npx prettier --write "$f": Formatea el archivo
  • "timeout": 30: Máximo 30 segundos

Ejemplo 2: Hook con script externo

Es más limpio separar la lógica en archivos:

Estructura de archivos:

.claude/
├── settings.json
└── hooks/
    └── format-typescript.sh

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-typescript.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

.claude/hooks/format-typescript.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# Leer el JSON de entrada desde stdin
file_path=$(jq -r '.tool_input.file_path')
 
# Verificar que es un archivo TypeScript/JavaScript
if [[ "$file_path" =~ \.(ts|tsx|js|jsx)$ ]]; then
  echo "Formateando: $file_path" >&2
  npx prettier --write "$file_path" 2>&1
fi
 
exit 0

No olvides dar permisos de ejecución:

chmod +x .claude/hooks/format-typescript.sh

Variables disponibles

Los hooks reciben información de dos formas:

1. Variables de entorno

Disponibles como $VARIABLE en bash:

VariableDescripciónDisponible en
$CLAUDE_PROJECT_DIRRuta absoluta al directorio del proyectoTodos los hooks
$CLAUDE_ENV_FILEArchivo para persistir variables de sesiónSessionStart
$CLAUDE_CODE_REMOTE"true" si está en modo remoto, vacío en CLITodos los hooks

Ejemplo:

#!/usr/bin/env bash
echo "Proyecto: $CLAUDE_PROJECT_DIR"
echo "Es remoto: $CLAUDE_CODE_REMOTE"

2. JSON de entrada (stdin)

Cada hook recibe un JSON con información contextual vía stdin.

Campos comunes (todos los hooks)

{
  "session_id": "abc123def456",
  "transcript_path": "/ruta/al/transcript.jsonl",
  "cwd": "/directorio/actual",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse"
}

Campos específicos para hooks de herramientas

PreToolUse / PostToolUse con Write:

{
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/ruta/absoluta/archivo.ts",
    "content": "contenido del archivo..."
  },
  "tool_use_id": "toolu_01ABC123..."
}

PreToolUse / PostToolUse con Edit:

{
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/ruta/absoluta/archivo.ts",
    "old_string": "texto antiguo",
    "new_string": "texto nuevo",
    "replace_all": false
  }
}

PreToolUse / PostToolUse con Bash:

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Ejecutar tests",
    "timeout": 120000,
    "run_in_background": false
  }
}

Cómo usar el JSON de entrada

Con jq (recomendado):

#!/usr/bin/env bash
 
# Extraer campos específicos
file_path=$(jq -r '.tool_input.file_path')
tool_name=$(jq -r '.tool_name')
command=$(jq -r '.tool_input.command // empty')
 
echo "Herramienta: $tool_name"
echo "Archivo: $file_path"

Sin jq (alternativa básica):

#!/usr/bin/env bash
 
# Leer todo el stdin
json_input=$(cat)
 
# Extraer con grep/sed (menos robusto)
file_path=$(echo "$json_input" | grep -oP '"file_path":\s*"\K[^"]+')

Códigos de salida y control de flujo

Los hooks se comunican usando códigos de salida:

CódigoSignificadoEfecto
0ÉxitoContinúa normalmente. stdout se muestra en modo verbose
2Error bloqueanteBLOQUEA la operación. stderr se muestra al usuario
1 u otrosError no bloqueanteSe registra pero no bloquea. stderr en verbose

Ejemplo: Bloquear operaciones peligrosas

#!/usr/bin/env bash
# .claude/hooks/block-dangerous.sh
 
command=$(jq -r '.tool_input.command // empty')
 
# Lista de comandos peligrosos
if echo "$command" | grep -qE '(rm -rf|sudo|mkfs|dd if=|:(){ :|:& };:)'; then
  echo "BLOQUEADO: Comando peligroso detectado" >&2
  exit 2  # Bloquea la ejecución
fi
 
# Comando permitido
exit 0

Configuración:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}

JSON de salida avanzado

Los hooks pueden devolver JSON en stdout (solo con exit 0) para comunicar decisiones complejas.

Para PreToolUse

#!/usr/bin/env bash
 
file_path=$(jq -r '.tool_input.file_path')
 
if [[ "$file_path" == *".env"* ]]; then
  # Denegar con JSON
  cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "No se pueden modificar archivos .env"
  }
}
EOF
  exit 0
fi
 
# Permitir
cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow"
  }
}
EOF
exit 0

Para UserPromptSubmit

#!/usr/bin/env bash
 
prompt=$(jq -r '.user_prompt' | tr '[:upper:]' '[:lower:]')
 
if echo "$prompt" | grep -qE '(password|secret|token)'; then
  # Bloquear prompt
  cat <<EOF
{
  "decision": "block",
  "reason": "El prompt contiene información sensible",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit"
  }
}
EOF
  exit 0
fi
 
# Permitir y añadir contexto
cat <<EOF
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "Branch actual: $(git branch --show-current 2>/dev/null || echo 'no-git')"
  }
}
EOF
exit 0

Timeouts

Cada hook puede tener un timeout personalizado:

{
  "type": "command",
  "command": "npm test",
  "timeout": 300
}

Valores:

  • Default: 600 segundos (10 minutos)
  • Mínimo recomendado: 10 segundos
  • Máximo: Sin límite técnico, pero considera la experiencia de usuario

Guía de timeouts recomendados

OperaciónTimeout recomendado
Formateo (prettier, black)30-60 segundos
Linting (eslint, pylint)60-120 segundos
Tests unitarios120-300 segundos
Tests de integración300-600 segundos
Operaciones de red60-180 segundos
Logging/auditoría10-30 segundos

Múltiples hooks para el mismo evento

Puedes configurar múltiples hooks que se ejecutan secuencialmente:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format.sh",
            "timeout": 30
          },
          {
            "type": "command",
            "command": ".claude/hooks/lint.sh",
            "timeout": 60
          },
          {
            "type": "command",
            "command": ".claude/hooks/test.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Orden de ejecución: De arriba hacia abajo.

Si un hook falla:

  • Exit 0: Continúa al siguiente
  • Exit 2: Bloquea y no ejecuta los siguientes
  • Exit 1 u otros: Registra error pero continúa

Ejemplo completo: Proyecto real

Configuración completa para un proyecto TypeScript/React:

.claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"SESSION_START=$(date +%s)\" > \"$CLAUDE_ENV_FILE\" && echo \"Sesión iniciada: $(date)\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/log-bash.sh",
            "timeout": 10
          },
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh",
            "timeout": 10
          }
        ]
      },
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-sensitive-files.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-and-lint.sh",
            "timeout": 60
          },
          {
            "type": "command",
            "command": ".claude/hooks/run-tests.sh",
            "timeout": 180
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude ha terminado'",
            "timeout": 5
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"Sesión finalizada: $(date)\" >> ~/.claude/session-history.log",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

.claude/hooks/log-bash.sh:

#!/usr/bin/env bash
set -euo pipefail
 
command=$(jq -r '.tool_input.command')
description=$(jq -r '.tool_input.description // "Sin descripción"')
 
echo "[$(date -Iseconds)] $description: $command" >> "$CLAUDE_PROJECT_DIR/.claude/bash-log.txt"
exit 0

.claude/hooks/block-dangerous.sh:

#!/usr/bin/env bash
set -euo pipefail
 
command=$(jq -r '.tool_input.command // empty')
 
if echo "$command" | grep -qE '(rm -rf|sudo |mkfs|dd if=)'; then
  echo "ERROR: Comando peligroso bloqueado" >&2
  exit 2
fi
 
exit 0

.claude/hooks/block-sensitive-files.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
if echo "$file_path" | grep -qE '(\.env|\.git/|\.ssh/|credentials|secrets)'; then
  echo "ERROR: Acceso denegado a archivo sensible: $file_path" >&2
  exit 2
fi
 
exit 0

.claude/hooks/format-and-lint.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo procesar archivos TypeScript/JavaScript
if [[ "$file_path" =~ \.(ts|tsx|js|jsx)$ ]]; then
  echo "Formateando: $file_path" >&2
  npx prettier --write "$file_path" 2>&1
 
  echo "Linting: $file_path" >&2
  npx eslint --fix "$file_path" 2>&1 || true  # No fallar si hay errores de lint
fi
 
exit 0

.claude/hooks/run-tests.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo ejecutar tests para archivos en src/
if [[ "$file_path" =~ ^.*/src/.* ]] && [[ "$file_path" =~ \.(ts|tsx)$ ]]; then
  echo "Ejecutando tests relacionados con: $file_path" >&2
  npm test -- --findRelatedTests "$file_path" --passWithNoTests 2>&1 || true
fi
 
exit 0

Mejores prácticas

1. Usar rutas absolutas

Correcto:

{
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/script.sh"
}

Incorrecto:

{
  "command": ".claude/hooks/script.sh"
}

2. Siempre entrecomillar variables

Correcto:

npx prettier --write "$file_path"

Incorrecto:

npx prettier --write $file_path  # Falla con espacios

3. Usar set -euo pipefail en scripts bash

#!/usr/bin/env bash
set -euo pipefail  # Salir en errores, variables no definidas, fallos en pipes
 
# Tu código aquí

4. Validar entrada

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path // empty')
 
# Verificar que no está vacío
if [[ -z "$file_path" ]]; then
  echo "ERROR: file_path no disponible" >&2
  exit 1
fi
 
# Verificar path traversal
if [[ "$file_path" == *".."* ]]; then
  echo "ERROR: Path traversal detectado" >&2
  exit 2
fi

5. Logging a stderr, output a stdout

#!/usr/bin/env bash
 
# Mensajes informativos a stderr (se muestran en verbose)
echo "Procesando archivo..." >&2
 
# Output JSON o datos a stdout (se usa para decisiones)
echo '{"status": "ok"}'

6. Usar timeouts apropiados

{
  "hooks": [
    {
      "type": "command",
      "command": "operación-rápida.sh",
      "timeout": 10
    },
    {
      "type": "command",
      "command": "operación-lenta.sh",
      "timeout": 300
    }
  ]
}

7. No fallar por errores menores

# Lint puede encontrar errores pero no debe bloquear
npx eslint --fix "$file_path" 2>&1 || true
 
# Tests pueden fallar pero no debe bloquear la edición
npm test -- --findRelatedTests "$file_path" 2>&1 || true

Debugging de hooks

Ver qué hooks se ejecutan

# Modo verbose: muestra todos los hooks y su output
claude --verbose
 
# O durante la sesión, presiona Ctrl+O

Logs de hooks

# Agregar logging explícito
echo "Hook ejecutado: $(date)" >> /tmp/claude-hooks.log
jq '.' >> /tmp/claude-hooks-input.log  # Guardar JSON de entrada

Probar hooks manualmente

# Simular entrada JSON
echo '{"tool_input": {"file_path": "/ruta/test.ts"}}' | .claude/hooks/script.sh

Verificar permisos

# Los scripts deben ser ejecutables
chmod +x .claude/hooks/*.sh
 
# Verificar
ls -la .claude/hooks/

Puntos clave

  1. Los hooks se configuran en settings.json usando estructura JSON
  2. Tres ubicaciones: global, proyecto, local
  3. Variables disponibles: entorno ($CLAUDE_PROJECT_DIR) y JSON stdin
  4. Códigos de salida: 0 (éxito), 2 (bloquear), otros (error)
  5. Timeouts configurables por hook (default: 600s)
  6. Es mejor usar scripts externos que comandos inline complejos
  7. Siempre validar entrada, entrecomillar variables y usar rutas absolutas
  8. Usa set -euo pipefail en scripts bash
  9. Logging a stderr, output de decisiones a stdout
  10. Modo verbose (--verbose o Ctrl+O) para debugging

Recursos adicionales


Próximo paso: 5.4 Casos de uso avanzados - Patrones y recetas para hooks complejos en escenarios reales.


5.4 Casos de uso avanzados

Objetivo

Explorar patrones avanzados de hooks para escenarios reales: automatización de commits, tests, notificaciones, multi-lenguaje, integración con herramientas externas y más.


Introducción

En este punto veremos implementaciones completas y funcionales de hooks para casos de uso reales. Cada ejemplo incluye código completo, configuración y explicación de las decisiones de diseño.


Caso de uso 1: Auto-commit después de cambios

Escenario: Quieres que Claude haga commit automáticamente después de cada edición, ideal para tener un historial granular de cambios.

Implementación

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/auto-commit.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

.claude/hooks/auto-commit.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
tool_name=$(jq -r '.tool_name')
 
# Verificar que estamos en un repositorio git
if ! git rev-parse --git-dir > /dev/null 2>&1; then
  echo "No es un repositorio git, omitiendo commit" >&2
  exit 0
fi
 
# Verificar que el archivo existe
if [[ ! -f "$file_path" ]]; then
  echo "Archivo no existe: $file_path" >&2
  exit 0
fi
 
# Obtener el nombre del archivo relativo al repo
relative_path=$(git ls-files --full-name "$file_path" 2>/dev/null || basename "$file_path")
 
# Crear mensaje de commit descriptivo
commit_msg="Auto: $tool_name $relative_path
 
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"
 
# Agregar y hacer commit
git add "$file_path"
git commit -m "$commit_msg" --quiet 2>&1 || {
  echo "Commit falló o no hay cambios" >&2
  exit 0
}
 
echo "✓ Commit automático realizado: $relative_path" >&2
exit 0

Mejoras opcionales

Agregar SHA del commit al log:

commit_sha=$(git rev-parse --short HEAD)
echo "[$(date -Iseconds)] Commit $commit_sha: $relative_path" >> "$CLAUDE_PROJECT_DIR/.claude/commit-log.txt"

Evitar commits vacíos:

# Verificar que hay cambios
if git diff --cached --quiet; then
  echo "No hay cambios para commitear" >&2
  exit 0
fi

Caso de uso 2: Ejecutar tests relacionados inteligentemente

Escenario: Ejecutar solo los tests relacionados con el archivo modificado, no toda la suite de tests.

Implementación multi-framework

.claude/hooks/run-related-tests.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo procesar archivos de código fuente
if [[ ! "$file_path" =~ \.(ts|tsx|js|jsx|py|go)$ ]]; then
  exit 0
fi
 
# No ejecutar tests para archivos de test
if [[ "$file_path" =~ \.(test|spec)\. ]]; then
  echo "Archivo de test modificado, omitiendo ejecución" >&2
  exit 0
fi
 
echo "Ejecutando tests relacionados con: $file_path" >&2
 
# Detectar el framework de testing
if [[ -f "package.json" ]] && grep -q '"jest"' package.json; then
  # Jest (JavaScript/TypeScript)
  npm test -- --findRelatedTests "$file_path" --passWithNoTests 2>&1 || {
    echo "⚠ Algunos tests fallaron" >&2
    exit 0  # No bloquear por tests fallidos
  }
elif [[ -f "pytest.ini" ]] || [[ -f "setup.py" ]]; then
  # Pytest (Python)
  pytest "$file_path" --tb=short 2>&1 || {
    echo "⚠ Algunos tests fallaron" >&2
    exit 0
  }
elif [[ "$file_path" =~ \.go$ ]]; then
  # Go testing
  package_dir=$(dirname "$file_path")
  go test "./$package_dir" -v 2>&1 || {
    echo "⚠ Algunos tests fallaron" >&2
    exit 0
  }
else
  echo "Framework de testing no detectado" >&2
  exit 0
fi
 
echo "✓ Tests completados" >&2
exit 0

Variante: Solo tests unitarios rápidos

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Jest con timeout corto y solo tests unitarios
npm test -- \
  --findRelatedTests "$file_path" \
  --testPathIgnorePatterns="e2e|integration" \
  --maxWorkers=2 \
  --testTimeout=5000 \
  --passWithNoTests \
  2>&1 || exit 0
 
exit 0

Caso de uso 3: Notificaciones de sistema avanzadas

Escenario: Notificaciones desktop con información contextual útil.

Implementación cross-platform

.claude/hooks/notify.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# Detectar sistema operativo
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
  NOTIFY_CMD="notify-send"
elif [[ "$OSTYPE" == "darwin"* ]]; then
  NOTIFY_CMD="osascript -e"
else
  echo "Sistema no soportado para notificaciones" >&2
  exit 0
fi
 
# Función para enviar notificación
send_notification() {
  local title="$1"
  local message="$2"
  local urgency="${3:-normal}"
 
  if [[ "$NOTIFY_CMD" == "notify-send" ]]; then
    notify-send -u "$urgency" "$title" "$message"
  else
    osascript -e "display notification \"$message\" with title \"$title\""
  fi
}
 
# Hook Stop: Claude terminó
if [[ "${HOOK_EVENT:-}" == "Stop" ]]; then
  # Calcular duración desde SessionStart
  if [[ -f "$CLAUDE_PROJECT_DIR/.claude/session-start" ]]; then
    start_time=$(cat "$CLAUDE_PROJECT_DIR/.claude/session-start")
    duration=$(($(date +%s) - start_time))
    send_notification "Claude Code" "Tarea completada en ${duration}s" "normal"
  else
    send_notification "Claude Code" "Claude ha terminado" "normal"
  fi
fi
 
exit 0

Notificación con preview de cambios

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
tool_name=$(jq -r '.tool_name')
 
# Obtener estadísticas del archivo
if [[ -f "$file_path" ]]; then
  lines=$(wc -l < "$file_path")
  size=$(du -h "$file_path" | cut -f1)
 
  notify-send "Claude Code: $tool_name" \
    "Archivo: $(basename "$file_path")\nLíneas: $lines\nTamaño: $size" \
    --icon=dialog-information
fi
 
exit 0

Caso de uso 4: Formateo y linting multi-lenguaje

Escenario: Formatear y lintear automáticamente según el lenguaje del archivo.

Implementación completa

.claude/hooks/format-multi-lang.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
extension="${file_path##*.}"
 
echo "Procesando: $file_path (.$extension)" >&2
 
case "$extension" in
  ts|tsx|js|jsx)
    echo "→ Prettier + ESLint" >&2
    npx prettier --write "$file_path" 2>&1
    npx eslint --fix "$file_path" 2>&1 || true
    ;;
 
  py)
    echo "→ Black + isort + Ruff" >&2
    python -m black "$file_path" 2>&1 || true
    python -m isort "$file_path" 2>&1 || true
    python -m ruff check --fix "$file_path" 2>&1 || true
    ;;
 
  go)
    echo "→ gofmt + goimports" >&2
    gofmt -w "$file_path" 2>&1
    goimports -w "$file_path" 2>&1 || true
    ;;
 
  rs)
    echo "→ rustfmt" >&2
    rustfmt "$file_path" 2>&1 || true
    ;;
 
  java)
    echo "→ google-java-format" >&2
    google-java-format --replace "$file_path" 2>&1 || true
    ;;
 
  md)
    echo "→ Prettier (Markdown)" >&2
    npx prettier --write "$file_path" 2>&1 || true
    ;;
 
  json|yaml|yml)
    echo "→ Prettier (config files)" >&2
    npx prettier --write "$file_path" 2>&1 || true
    ;;
 
  *)
    echo "No hay formateador configurado para .$extension" >&2
    ;;
esac
 
echo "✓ Formateo completado" >&2
exit 0

Configuración con cache

.claude/hooks/format-with-cache.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
cache_dir="$CLAUDE_PROJECT_DIR/.claude/cache"
mkdir -p "$cache_dir"
 
# Calcular hash del archivo
file_hash=$(md5sum "$file_path" | cut -d' ' -f1)
cache_file="$cache_dir/$(basename "$file_path").$file_hash"
 
# Si ya formateamos este contenido, skip
if [[ -f "$cache_file" ]]; then
  echo "Cache hit, omitiendo formateo" >&2
  exit 0
fi
 
# Formatear
if [[ "$file_path" =~ \.(ts|tsx|js|jsx)$ ]]; then
  npx prettier --write "$file_path" 2>&1
fi
 
# Marcar como formateado
touch "$cache_file"
 
# Limpiar cache viejo (más de 7 días)
find "$cache_dir" -type f -mtime +7 -delete 2>/dev/null || true
 
exit 0

Caso de uso 5: Integración con Slack/Discord

Escenario: Notificar al equipo cuando Claude complete tareas importantes.

Webhook de Slack

.claude/hooks/notify-slack.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# Configurar webhook URL (usar settings.local.json para no commitear)
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
 
if [[ -z "$SLACK_WEBHOOK_URL" ]]; then
  echo "SLACK_WEBHOOK_URL no configurado" >&2
  exit 0
fi
 
# Obtener información del contexto
session_id=$(jq -r '.session_id // "unknown"')
project_name=$(basename "$CLAUDE_PROJECT_DIR")
user=$(whoami)
branch=$(git branch --show-current 2>/dev/null || echo "no-git")
 
# Construir mensaje
message="Claude Code completó tarea en *$project_name*
• Usuario: $user
• Branch: $branch
• Session: ${session_id:0:8}"
 
# Enviar a Slack
curl -X POST "$SLACK_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d "{\"text\":\"$message\"}" \
  --silent --show-error \
  2>&1 || {
    echo "Fallo al enviar a Slack" >&2
    exit 0
  }
 
echo "✓ Notificación enviada a Slack" >&2
exit 0

.claude/settings.local.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "SLACK_WEBHOOK_URL='https://hooks.slack.com/services/YOUR/WEBHOOK/URL' .claude/hooks/notify-slack.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Webhook de Discord

.claude/hooks/notify-discord.sh:

#!/usr/bin/env bash
set -euo pipefail
 
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
 
if [[ -z "$DISCORD_WEBHOOK_URL" ]]; then
  exit 0
fi
 
project_name=$(basename "$CLAUDE_PROJECT_DIR")
branch=$(git branch --show-current 2>/dev/null || echo "no-git")
 
# Discord usa formato JSON diferente
payload=$(cat <<EOF
{
  "embeds": [{
    "title": "Claude Code: Tarea completada",
    "description": "Proyecto: **$project_name**\nBranch: \`$branch\`",
    "color": 5814783,
    "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
  }]
}
EOF
)
 
curl -X POST "$DISCORD_WEBHOOK_URL" \
  -H 'Content-Type: application/json' \
  -d "$payload" \
  --silent 2>&1 || exit 0
 
exit 0

Caso de uso 6: Pre-commit validation

Escenario: Validar que el código cumple estándares antes de permitir commits.

.claude/hooks/pre-commit-validation.sh:

#!/usr/bin/env bash
set -euo pipefail
 
command=$(jq -r '.tool_input.command // empty')
 
# Solo validar comandos git commit
if [[ ! "$command" =~ ^git[[:space:]]+commit ]]; then
  exit 0
fi
 
echo "Validando código antes de commit..." >&2
 
# 1. Verificar que no hay archivos sensibles staged
sensitive_files=$(git diff --cached --name-only | grep -E '\.(env|pem|key|p12)$' || true)
if [[ -n "$sensitive_files" ]]; then
  echo "ERROR: Archivos sensibles detectados en stage:" >&2
  echo "$sensitive_files" >&2
  exit 2  # Bloquear commit
fi
 
# 2. Ejecutar linters en archivos staged
echo "Ejecutando linters..." >&2
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
 
for file in $staged_files; do
  if [[ -f "$file" ]]; then
    case "$file" in
      *.ts|*.tsx|*.js|*.jsx)
        npx eslint "$file" 2>&1 || {
          echo "ERROR: ESLint falló en $file" >&2
          exit 2
        }
        ;;
      *.py)
        python -m ruff check "$file" 2>&1 || {
          echo "ERROR: Ruff falló en $file" >&2
          exit 2
        }
        ;;
    esac
  fi
done
 
# 3. Ejecutar tests
echo "Ejecutando tests..." >&2
npm test -- --passWithNoTests --bail 2>&1 || {
  echo "ERROR: Tests fallaron" >&2
  exit 2
}
 
echo "✓ Validación completada" >&2
exit 0

Caso de uso 7: Code coverage tracking

Escenario: Monitorear la cobertura de código y alertar si baja.

.claude/hooks/track-coverage.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
coverage_dir="$CLAUDE_PROJECT_DIR/.claude/coverage"
mkdir -p "$coverage_dir"
 
# Solo para archivos de código
if [[ ! "$file_path" =~ \.(ts|tsx|js|jsx)$ ]]; then
  exit 0
fi
 
# Ejecutar tests con coverage
echo "Calculando cobertura..." >&2
npm test -- \
  --coverage \
  --coverageReporters=json-summary \
  --collectCoverageFrom="$file_path" \
  --passWithNoTests \
  > /dev/null 2>&1 || exit 0
 
# Leer cobertura
if [[ -f "coverage/coverage-summary.json" ]]; then
  coverage=$(jq -r '.total.lines.pct' coverage/coverage-summary.json 2>/dev/null || echo "0")
 
  # Guardar historial
  echo "$(date -Iseconds),$file_path,$coverage" >> "$coverage_dir/history.csv"
 
  # Alertar si baja de 80%
  if (( $(echo "$coverage < 80" | bc -l) )); then
    echo "⚠ Cobertura baja: ${coverage}% en $file_path" >&2
    notify-send "Claude Code: Cobertura baja" \
      "Archivo: $(basename "$file_path")\nCobertura: ${coverage}%" \
      --urgency=critical 2>/dev/null || true
  else
    echo "✓ Cobertura: ${coverage}%" >&2
  fi
fi
 
exit 0

Caso de uso 8: Documentación automática

Escenario: Generar/actualizar documentación cuando se modifican archivos.

.claude/hooks/auto-document.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo documentar archivos en src/
if [[ ! "$file_path" =~ ^.*/src/.* ]]; then
  exit 0
fi
 
# Solo para archivos de código
if [[ ! "$file_path" =~ \.(ts|tsx|js|jsx|py)$ ]]; then
  exit 0
fi
 
# Generar documentación con typedoc o pydoc
if [[ "$file_path" =~ \.(ts|tsx)$ ]] && command -v typedoc > /dev/null; then
  echo "Generando documentación TypeScript..." >&2
  npx typedoc --entryPoints "$file_path" --out docs/api --skipErrorChecking 2>&1 || true
 
elif [[ "$file_path" =~ \.py$ ]] && command -v pydoc > /dev/null; then
  echo "Generando documentación Python..." >&2
  module_name=$(basename "$file_path" .py)
  pydoc -w "$module_name" 2>&1 || true
fi
 
# Actualizar README con TOC
if [[ -f "README.md" ]]; then
  echo "Actualizando tabla de contenidos..." >&2
  npx markdown-toc -i README.md 2>&1 || true
fi
 
exit 0

Caso de uso 9: Security scanning

Escenario: Escanear archivos modificados en busca de vulnerabilidades.

.claude/hooks/security-scan.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
echo "Escaneando seguridad: $file_path" >&2
 
# 1. Buscar secretos hardcodeados
if command -v trufflehog > /dev/null; then
  trufflehog filesystem "$file_path" --json 2>&1 | \
    jq -r 'select(.Verified == true) | "⚠ SECRET ENCONTRADO: \(.DetectorName)"' || true
fi
 
# 2. Escanear vulnerabilidades conocidas
case "$file_path" in
  *.py)
    if command -v bandit > /dev/null; then
      bandit -r "$file_path" -f json 2>&1 | \
        jq -r '.results[] | select(.issue_severity == "HIGH" or .issue_severity == "CRITICAL") |
          "⚠ \(.issue_severity): \(.issue_text)"' || true
    fi
    ;;
 
  *.js|*.ts)
    if [[ -f "package.json" ]]; then
      npm audit --json 2>&1 | \
        jq -r '.vulnerabilities | to_entries[] | select(.value.severity == "high" or .value.severity == "critical") |
          "⚠ \(.value.severity): \(.key)"' || true
    fi
    ;;
esac
 
# 3. Verificar que no hay eval() o exec()
if grep -nE '(eval\(|exec\(|system\(|shell_exec)' "$file_path" 2>/dev/null; then
  echo "⚠ Funciones peligrosas detectadas en $file_path" >&2
fi
 
exit 0

Caso de uso 10: Performance monitoring

Escenario: Monitorear el rendimiento de hooks y alertar si son muy lentos.

.claude/hooks/performance-wrapper.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# Este wrapper ejecuta otro hook y mide su tiempo
TARGET_HOOK="${1:-}"
THRESHOLD_SECONDS="${2:-5}"
 
if [[ -z "$TARGET_HOOK" ]]; then
  echo "Uso: performance-wrapper.sh <hook-script> [threshold-seconds]" >&2
  exit 1
fi
 
# Leer JSON de stdin y guardarlo
json_input=$(cat)
 
# Ejecutar hook con timing
start=$(date +%s.%N)
echo "$json_input" | "$TARGET_HOOK"
exit_code=$?
end=$(date +%s.%N)
 
# Calcular duración
duration=$(echo "$end - $start" | bc)
 
# Registrar performance
perf_log="$CLAUDE_PROJECT_DIR/.claude/performance.log"
echo "$(date -Iseconds),$TARGET_HOOK,$duration,$exit_code" >> "$perf_log"
 
# Alertar si excede el threshold
if (( $(echo "$duration > $THRESHOLD_SECONDS" | bc -l) )); then
  echo "⚠ Hook lento detectado: $TARGET_HOOK tomó ${duration}s" >&2
  notify-send "Claude Code: Hook lento" \
    "Hook: $(basename "$TARGET_HOOK")\nDuración: ${duration}s" \
    --urgency=normal 2>/dev/null || true
fi
 
exit $exit_code

Uso en settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/performance-wrapper.sh .claude/hooks/format.sh 3",
            "timeout": 90
          }
        ]
      }
    ]
  }
}

Caso de uso 11: Hooks con estado persistente

Escenario: Mantener estado entre ejecuciones de hooks (contadores, estadísticas).

.claude/hooks/track-changes.sh:

#!/usr/bin/env bash
set -euo pipefail
 
state_file="$CLAUDE_PROJECT_DIR/.claude/state.json"
 
# Inicializar estado si no existe
if [[ ! -f "$state_file" ]]; then
  echo '{"files_modified": 0, "files_created": 0, "total_lines": 0}' > "$state_file"
fi
 
# Leer estado actual
files_modified=$(jq -r '.files_modified' "$state_file")
files_created=$(jq -r '.files_created' "$state_file")
total_lines=$(jq -r '.total_lines' "$state_file")
 
# Procesar evento actual
file_path=$(jq -r '.tool_input.file_path')
tool_name=$(jq -r '.tool_name')
 
if [[ "$tool_name" == "Write" ]] && [[ ! -f "$file_path" ]]; then
  files_created=$((files_created + 1))
elif [[ "$tool_name" == "Edit" ]]; then
  files_modified=$((files_modified + 1))
fi
 
# Contar líneas
if [[ -f "$file_path" ]]; then
  lines=$(wc -l < "$file_path")
  total_lines=$((total_lines + lines))
fi
 
# Guardar estado actualizado
jq -n \
  --arg fm "$files_modified" \
  --arg fc "$files_created" \
  --arg tl "$total_lines" \
  '{files_modified: ($fm|tonumber), files_created: ($fc|tonumber), total_lines: ($tl|tonumber)}' \
  > "$state_file"
 
echo "📊 Estadísticas: $files_modified editados, $files_created creados, $total_lines líneas totales" >&2
exit 0

Ver estadísticas:

cat .claude/state.json | jq

Caso de uso 12: Hooks condicionales

Escenario: Ejecutar hooks solo en ciertos contextos (branches, directorios, horarios).

.claude/hooks/conditional-tests.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Condición 1: Solo ejecutar en branch main/master
current_branch=$(git branch --show-current 2>/dev/null || echo "unknown")
if [[ ! "$current_branch" =~ ^(main|master)$ ]]; then
  echo "No estás en main/master, omitiendo tests exhaustivos" >&2
  exit 0
fi
 
# Condición 2: Solo archivos críticos
if [[ ! "$file_path" =~ (src/core|src/auth|src/payment) ]]; then
  echo "No es código crítico, omitiendo tests exhaustivos" >&2
  exit 0
fi
 
# Condición 3: Solo durante horario laboral
hour=$(date +%H)
if (( hour < 9 || hour > 18 )); then
  echo "Fuera de horario laboral, omitiendo tests lentos" >&2
  exit 0
fi
 
# Ejecutar tests completos
echo "Ejecutando suite completa de tests..." >&2
npm test -- --coverage 2>&1 || exit 0
 
exit 0

Mejores prácticas para casos avanzados

1. Manejo de errores robusto

#!/usr/bin/env bash
set -euo pipefail
 
# Trap para cleanup
cleanup() {
  local exit_code=$?
  echo "Limpiando recursos..." >&2
  # Cleanup aquí
  exit $exit_code
}
trap cleanup EXIT
 
# Tu código aquí

2. Timeouts internos

# Ejecutar comando con timeout interno
timeout 30s npm test || {
  echo "Timeout alcanzado" >&2
  exit 0
}

3. Logging estructurado

log() {
  local level="$1"
  shift
  echo "$(date -Iseconds) [$level] $*" >> "$CLAUDE_PROJECT_DIR/.claude/hooks.log"
}
 
log INFO "Iniciando hook"
log ERROR "Algo falló"

4. Testing de hooks

# test-hook.sh
#!/usr/bin/env bash
 
# Simular entrada JSON
test_input='{"tool_input": {"file_path": "/tmp/test.ts"}, "tool_name": "Write"}'
 
# Ejecutar hook
echo "$test_input" | .claude/hooks/your-hook.sh
 
# Verificar resultado
if [[ $? -eq 0 ]]; then
  echo "✓ Test pasó"
else
  echo "✗ Test falló"
  exit 1
fi

Plantilla de hook avanzado

Usa esta plantilla como punto de partida:

#!/usr/bin/env bash
set -euo pipefail
 
# =============================================================================
# Hook: [Nombre del hook]
# Descripción: [Qué hace este hook]
# =============================================================================
 
# --- Configuración ---
TIMEOUT=30
LOG_FILE="$CLAUDE_PROJECT_DIR/.claude/hook-name.log"
 
# --- Funciones auxiliares ---
log() {
  echo "$(date -Iseconds) [$1] ${*:2}" | tee -a "$LOG_FILE" >&2
}
 
cleanup() {
  log INFO "Limpiando recursos"
  # Cleanup aquí
}
trap cleanup EXIT
 
# --- Validación de entrada ---
if ! jq -e . >/dev/null 2>&1; then
  log ERROR "JSON de entrada inválido"
  exit 1
fi
 
# --- Extracción de datos ---
file_path=$(jq -r '.tool_input.file_path // empty')
tool_name=$(jq -r '.tool_name // empty')
 
if [[ -z "$file_path" ]]; then
  log WARN "file_path no disponible"
  exit 0
fi
 
# --- Validaciones condicionales ---
if [[ ! -f "$file_path" ]]; then
  log WARN "Archivo no existe: $file_path"
  exit 0
fi
 
# Solo procesar ciertos tipos de archivos
if [[ ! "$file_path" =~ \.(ts|tsx|js|jsx)$ ]]; then
  log INFO "Tipo de archivo no soportado: $file_path"
  exit 0
fi
 
# --- Lógica principal ---
log INFO "Procesando: $file_path"
 
# Tu código aquí
# ...
 
log INFO "Completado exitosamente"
exit 0

Depuración de hooks avanzados

Ver logs de hooks

# Ver últimos logs
tail -f .claude/hooks.log
 
# Ver solo errores
grep ERROR .claude/hooks.log
 
# Ver estadísticas de performance
sort -t, -k3 -n .claude/performance.log | tail -10

Probar hooks individualmente

# Crear JSON de prueba
cat > /tmp/test-input.json <<EOF
{
  "tool_input": {
    "file_path": "/ruta/a/archivo.ts",
    "command": "npm test"
  },
  "tool_name": "Write",
  "session_id": "test-session"
}
EOF
 
# Ejecutar hook
cat /tmp/test-input.json | .claude/hooks/your-hook.sh

Puntos clave

  1. Los hooks avanzados pueden integrarse con cualquier herramienta externa
  2. Usa JSON de entrada/salida para comunicación estructurada
  3. Implementa manejo de errores robusto con traps y cleanup
  4. Registra métricas (performance, contadores, logs)
  5. Hooks condicionales ahorran tiempo en contextos específicos
  6. Testing de hooks es fundamental para reliability
  7. Usa timeouts y validaciones para evitar bloqueos
  8. Documenta cada hook con su propósito y casos de uso
  9. Mantén estado persistente cuando sea necesario
  10. Monitorea performance de hooks para optimizar

Recursos adicionales


Próximo paso: 5.5 Timeout de hooks - Configuración, mejores prácticas y manejo de hooks lentos.


5.5 Timeout de hooks

Objetivo

Comprender cómo funcionan los timeouts en hooks, configurarlos apropiadamente, optimizar hooks lentos y evitar que bloqueen tu flujo de trabajo.


Introducción

Los hooks son poderosos, pero si son demasiado lentos pueden hacer que trabajar con Claude Code se sienta lento y frustrante. Los timeouts son el mecanismo de seguridad que evita que hooks problemáticos bloqueen indefinidamente tu sesión.

En este punto aprenderás a configurar timeouts apropiados, identificar hooks lentos y optimizar su rendimiento.


¿Qué es un timeout de hook?

Un timeout es el tiempo máximo que Claude Code esperará a que un hook termine su ejecución antes de matarlo forzosamente.

Comportamiento del timeout

Hook inicia ejecución

Pasan 10 segundos

Pasan 30 segundos

Pasan 60 segundos (timeout alcanzado)

Hook es terminado con SIGTERM

Si no responde en 5 segundos → SIGKILL

Claude Code continúa

Resultado: El hook es terminado, se registra el timeout, y la sesión continúa normalmente.


Configuración de timeouts

Valor por defecto

Sin configuración explícita: 600 segundos (10 minutos)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "./script.sh"
            // timeout: 600 (default implícito)
          }
        ]
      }
    ]
  }
}

Configuración explícita

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "command": "./script.sh",
            "timeout": 30  // 30 segundos
          }
        ]
      }
    ]
  }
}

Unidades: Segundos (entero positivo)


Guía de timeouts recomendados

Basado en la operación que realiza el hook:

Tipo de operaciónTimeout recomendadoJustificación
Formateo (Prettier, Black)30-60sOperación rápida en archivos individuales
Linting (ESLint, Pylint)60-120sPuede analizar dependencias
Tests unitarios120-300sDepende del número de tests
Tests de integración300-600sPueden involucrar DB, red, etc.
Compilación180-600sProyectos grandes tardan más
Operaciones de red60-180sAPIs externas, webhooks
Logging/auditoría10-30sOperaciones I/O simples
Notificaciones5-15sDeben ser instantáneas
Security scans120-300sAnálisis profundo de código

Regla general

Si tu hook tarda más de 2 minutos regularmente, probablemente necesita optimización o debería ejecutarse de forma diferente.


Síntomas de hooks lentos

Cómo detectar que tienes un problema

Síntomas en la sesión:

  • Claude Code se siente "trabado" después de ediciones
  • Hay pausas largas antes de que Claude responda
  • Ves mensajes de "Hook ejecutándose..." por mucho tiempo

Síntomas técnicos:

  • Hooks alcanzan su timeout frecuentemente
  • Logs muestran duraciones >60 segundos
  • CPU usage alto durante minutos después de ediciones

Verificar performance de hooks

Ver hooks en ejecución:

# Durante sesión, presiona Ctrl+O para modo verbose
# Verás output de hooks en tiempo real

Revisar logs de performance (si implementaste logging):

# Ver hooks más lentos
cat .claude/performance.log | sort -t, -k3 -rn | head -10
 
# Ver promedio de duración
awk -F, '{sum+=$3; count++} END {print "Promedio:", sum/count "s"}' .claude/performance.log

Estrategias de optimización

1. Operaciones paralelas → secuenciales

Problema: Ejecutar múltiples hooks pesados en cada edición

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {"type": "command", "command": "npm run format", "timeout": 60},
          {"type": "command", "command": "npm run lint", "timeout": 120},
          {"type": "command", "command": "npm test", "timeout": 300},
          {"type": "command", "command": "npm run build", "timeout": 600}
        ]
      }
    ]
  }
}

Total potencial: Hasta 1080 segundos (18 minutos) por edición

Solución: Priorizar y ejecutar solo lo esencial

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {"type": "command", "command": "./quick-format.sh", "timeout": 30}
          // Tests y build se ejecutan manualmente o en CI/CD
        ]
      }
    ]
  }
}

2. Filtrado inteligente

Problema: Hook ejecuta en todos los archivos

#!/usr/bin/env bash
# Ejecuta prettier en TODO el proyecto
npx prettier --write .

Solución: Procesar solo el archivo modificado

#!/usr/bin/env bash
file_path=$(jq -r '.tool_input.file_path')
 
# Solo formatear archivos relevantes
if [[ "$file_path" =~ \.(ts|tsx|js|jsx)$ ]]; then
  npx prettier --write "$file_path"
fi

3. Cache para evitar trabajo duplicado

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
cache_dir="$CLAUDE_PROJECT_DIR/.claude/cache"
mkdir -p "$cache_dir"
 
# Hash del contenido
content_hash=$(md5sum "$file_path" | cut -d' ' -f1)
cache_key="$cache_dir/$(basename "$file_path").$content_hash"
 
# Si ya procesamos este contenido, skip
if [[ -f "$cache_key" ]]; then
  echo "Cache hit, skipping" >&2
  exit 0
fi
 
# Hacer el trabajo
npx prettier --write "$file_path"
 
# Marcar como procesado
touch "$cache_key"
 
# Limpiar cache viejo (>7 días)
find "$cache_dir" -type f -mtime +7 -delete 2>/dev/null || true
 
exit 0

Resultado: Primera ejecución tarda normal, siguientes son instantáneas.


4. Skip en archivos no modificados

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Verificar si el archivo realmente cambió
if git diff --quiet "$file_path" 2>/dev/null; then
  echo "Archivo sin cambios, skipping tests" >&2
  exit 0
fi
 
# Ejecutar tests
npm test -- --findRelatedTests "$file_path"

5. Límites de ejecución

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo ejecutar tests unitarios (rápidos)
npm test -- \
  --findRelatedTests "$file_path" \
  --testPathIgnorePatterns="e2e|integration" \
  --maxWorkers=2 \
  --testTimeout=5000 \
  --passWithNoTests
 
exit 0

6. Background execution

Para operaciones realmente pesadas, usa background:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
project_dir="$CLAUDE_PROJECT_DIR"
 
# Ejecutar tests completos en background
(
  cd "$project_dir"
  npm test -- --coverage > .claude/test-results.log 2>&1
 
  # Notificar cuando termine
  notify-send "Tests completados" "Revisa .claude/test-results.log"
) &
 
# Hook termina inmediatamente
echo "Tests iniciados en background" >&2
exit 0

Pros: No bloquea la sesión Cons: No sabes resultado inmediatamente


Hooks condicionales para ahorrar tiempo

Solo ejecutar en ciertos contextos

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Condición 1: Branch
branch=$(git branch --show-current 2>/dev/null || echo "unknown")
if [[ ! "$branch" =~ ^(main|master|develop)$ ]]; then
  echo "Not on main branch, skipping expensive checks" >&2
  exit 0
fi
 
# Condición 2: Tamaño de archivo
size=$(wc -l < "$file_path")
if (( size > 1000 )); then
  echo "File too large, skipping linting" >&2
  exit 0
fi
 
# Condición 3: Tipo de archivo crítico
if [[ ! "$file_path" =~ (src/core|src/auth) ]]; then
  echo "Not critical code, skipping full test suite" >&2
  exit 0
fi
 
# Ejecutar checks completos solo si cumple todas las condiciones
npm run lint
npm test -- --coverage

Manejo de timeouts alcanzados

Qué sucede cuando un hook alcanza el timeout

  1. Claude Code envía SIGTERM al proceso del hook
  2. El hook tiene ~5 segundos para terminar limpiamente
  3. Si no termina, Claude Code envía SIGKILL (forzoso)
  4. El timeout se registra en logs
  5. La sesión continúa normalmente

Implementar cleanup al recibir señales

#!/usr/bin/env bash
set -euo pipefail
 
# Trap para manejar SIGTERM (timeout)
cleanup() {
  local exit_code=$?
  echo "Hook interrumpido (timeout o señal), limpiando..." >&2
 
  # Limpiar archivos temporales
  rm -f /tmp/hook-$$-*
 
  # Matar procesos hijos
  pkill -P $$ 2>/dev/null || true
 
  exit $exit_code
}
trap cleanup EXIT SIGTERM SIGINT
 
# Tu código aquí
echo "Ejecutando operación..." >&2
npm test
 
exit 0

Configuración por tipo de proyecto

Proyecto pequeño (< 10k líneas)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"type": "command", "command": ".claude/hooks/format.sh", "timeout": 30},
          {"type": "command", "command": ".claude/hooks/lint.sh", "timeout": 60},
          {"type": "command", "command": ".claude/hooks/test.sh", "timeout": 120}
        ]
      }
    ]
  }
}

Justificación: Proyecto pequeño, operaciones rápidas, tests completos.


Proyecto mediano (10k-100k líneas)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"type": "command", "command": ".claude/hooks/format.sh", "timeout": 45},
          {"type": "command", "command": ".claude/hooks/lint-file.sh", "timeout": 90},
          {"type": "command", "command": ".claude/hooks/test-related.sh", "timeout": 180}
        ]
      }
    ]
  }
}

Justificación: Solo lintear archivo modificado, solo tests relacionados.


Proyecto grande (> 100k líneas)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"type": "command", "command": ".claude/hooks/format-cached.sh", "timeout": 30}
          // Linting y tests se ejecutan en CI/CD, no en hooks
        ]
      }
    ]
  }
}

Justificación: Solo formateo con cache, todo lo demás en CI/CD.


Debugging de problemas de timeout

Paso 1: Identificar el hook problemático

# Activar modo verbose
claude --verbose
 
# O durante sesión: Ctrl+O
 
# Verás output como:
# [Hook PostToolUse] Iniciando: .claude/hooks/test.sh
# [Hook PostToolUse] Duración: 145.2s
# [Hook PostToolUse] Timeout alcanzado: .claude/hooks/test.sh

Paso 2: Medir manualmente

# Simular entrada del hook
cat > /tmp/test-input.json <<EOF
{
  "tool_input": {"file_path": "/ruta/a/archivo.ts"},
  "tool_name": "Write"
}
EOF
 
# Ejecutar con timing
time cat /tmp/test-input.json | .claude/hooks/test.sh
 
# Output:
# real    2m15.432s  ← Duración real
# user    1m45.234s
# sys     0m5.123s

Paso 3: Perfilar el hook

Añadir logging de timing interno:

#!/usr/bin/env bash
set -euo pipefail
 
log_time() {
  echo "[$(date +%H:%M:%S.%3N)] $*" >&2
}
 
log_time "Hook iniciado"
 
file_path=$(jq -r '.tool_input.file_path')
log_time "JSON parseado"
 
log_time "Iniciando formateo"
npx prettier --write "$file_path"
log_time "Formateo completado"
 
log_time "Iniciando linting"
npx eslint --fix "$file_path"
log_time "Linting completado"
 
log_time "Hook completado"
exit 0

Output en modo verbose:

[14:23:45.123] Hook iniciado
[14:23:45.234] JSON parseado
[14:23:45.235] Iniciando formateo
[14:23:48.456] Formateo completado
[14:23:48.457] Iniciando linting
[14:24:32.789] Linting completado
[14:24:32.790] Hook completado

Conclusión: El linting tarda 44 segundos, es el cuello de botella.


Paso 4: Optimizar la operación lenta

# Antes: Lint de todo el proyecto
npx eslint --fix .
 
# Después: Solo el archivo modificado
npx eslint --fix "$file_path"
 
# Ahora tarda: 2-3 segundos

Mejores prácticas de timeout

1. Empezar conservador, ajustar con datos

// Primera versión: timeout generoso
{"timeout": 300}
 
// Después de medir: promedio 45s, máximo 80s
{"timeout": 120}  // 50% buffer sobre máximo observado

2. Timeouts diferentes por operación

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"command": "./format.sh", "timeout": 30},   // Rápido
          {"command": "./lint.sh", "timeout": 90},     // Medio
          {"command": "./test.sh", "timeout": 180}     // Lento
        ]
      }
    ]
  }
}

3. Alertar antes de timeout

#!/usr/bin/env bash
set -euo pipefail
 
TIMEOUT=120
WARN_AT=90
 
# Ejecutar comando en background con monitoreo
(
  sleep $WARN_AT
  echo "⚠ Hook llevando mucho tiempo (${WARN_AT}s)" >&2
) &
warn_pid=$!
 
# Ejecutar operación principal
npm test
 
# Cancelar warning si terminamos a tiempo
kill $warn_pid 2>/dev/null || true
 
exit 0

4. Implementar timeout interno

#!/usr/bin/env bash
set -euo pipefail
 
# Timeout interno más agresivo que el de Claude Code
timeout 90s npm test || {
  echo "Timeout interno alcanzado, abortando" >&2
  exit 0  # No fallar, solo skip
}
 
exit 0

5. Degradación graceful

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Intentar tests completos con timeout corto
if timeout 60s npm test -- --findRelatedTests "$file_path" 2>/dev/null; then
  echo "✓ Tests completos pasaron" >&2
else
  # Fallback: solo smoke tests
  echo "Timeout en tests completos, ejecutando smoke tests" >&2
  timeout 15s npm test -- --testNamePattern="smoke" 2>/dev/null || true
fi
 
exit 0

Checklist de optimización

Usa esta lista para revisar hooks lentos:

  • El hook procesa solo el archivo modificado, no todo el proyecto
  • Tiene cache para evitar trabajo duplicado
  • Usa filtros para skip de archivos irrelevantes
  • Implementa cleanup al recibir señales (SIGTERM)
  • Tiene logging de timing para identificar cuellos de botella
  • El timeout es apropiado para la operación (ni muy corto, ni muy largo)
  • Operaciones muy lentas están en background o en CI/CD
  • Hay condiciones para skip en contextos no críticos
  • No ejecuta tests completos en cada edición
  • No compila todo el proyecto en cada edición

Cuándo mover operaciones a CI/CD

Si tu hook cumple alguno de estos criterios, considera moverlo a CI/CD:

  1. Tarda más de 3 minutos regularmente
  2. Requiere recursos significativos (CPU, memoria, red)
  3. Solo necesita ejecutarse en código finalizado, no durante desarrollo
  4. Ejecuta tests de integración o E2E
  5. Compila todo el proyecto
  6. Hace operaciones de red (deploy, notificaciones externas complejas)
  7. Genera reportes o artefactos pesados

Regla de oro: Los hooks deben ser rápidos y específicos. Si es lento y global, va en CI/CD.


Ejemplo completo: Hooks optimizados

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/quick-format.sh",
            "timeout": 30
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/background-checks.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

.claude/hooks/quick-format.sh (optimizado):

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Cache
cache_dir="$CLAUDE_PROJECT_DIR/.claude/cache"
mkdir -p "$cache_dir"
content_hash=$(md5sum "$file_path" | cut -d' ' -f1)
cache_key="$cache_dir/format.$(basename "$file_path").$content_hash"
 
if [[ -f "$cache_key" ]]; then
  exit 0
fi
 
# Solo formatear tipos soportados
case "$file_path" in
  *.ts|*.tsx|*.js|*.jsx)
    timeout 25s npx prettier --write "$file_path" 2>&1 || true
    ;;
  *.py)
    timeout 25s python -m black "$file_path" 2>&1 || true
    ;;
esac
 
touch "$cache_key"
find "$cache_dir" -type f -mtime +7 -delete 2>/dev/null || true
 
exit 0

.claude/hooks/background-checks.sh:

#!/usr/bin/env bash
 
# Ejecutar checks pesados en background (no bloquea sesión)
(
  cd "$CLAUDE_PROJECT_DIR"
  npm test -- --coverage > .claude/test-results.log 2>&1
  npm run lint > .claude/lint-results.log 2>&1
 
  # Notificar
  notify-send "Claude Code" "Checks completos disponibles en .claude/"
) &
 
echo "Checks iniciados en background" >&2
exit 0

Puntos clave

  1. Timeout por defecto: 600 segundos (10 minutos)
  2. Configura timeouts apropiados según la operación (30s-600s)
  3. Optimiza hooks lentos: cache, filtros, solo archivo modificado
  4. No ejecutes operaciones globales en hooks (tests completos, builds)
  5. Usa background execution para operaciones muy pesadas
  6. Implementa cleanup al recibir SIGTERM
  7. Mide performance con logging y ajusta timeouts con datos reales
  8. Degrada gracefully si operaciones tardan mucho
  9. Mueve a CI/CD lo que tarde >3 minutos o sea global
  10. Hooks deben ser rápidos y específicos, no lentos y globales

Recursos adicionales

  • Documentación oficial de hooks
  • Optimización de performance
  • CI/CD integration guide

Felicidades, has completado el Módulo 5: Hooks

Has aprendido:

  • ✅ Qué son los hooks y cuándo usarlos
  • ✅ Todos los tipos de hooks disponibles
  • ✅ Configuración en settings.json
  • ✅ Casos de uso avanzados con código completo
  • ✅ Optimización de performance y timeouts

Próximo paso: Módulo 6: MCP (Model Context Protocol) - Conecta Claude con herramientas y servicios externos.


Práctica del Módulo 5: Hooks

Objetivo

Aplicar todo lo aprendido sobre hooks configurando un sistema completo de automatización para un proyecto real.


Descripción

En esta práctica crearás una configuración completa de hooks para un proyecto de ejemplo, implementando:

  • Formateo automático
  • Linting
  • Tests relacionados
  • Logging y auditoría
  • Notificaciones
  • Seguridad

Prerequisitos

  • Claude Code instalado y configurado
  • Node.js 18+ (para proyecto de ejemplo)
  • Git configurado
  • Herramientas opcionales: notify-send (Linux) o similar

Proyecto de ejemplo

Usaremos un proyecto TypeScript/React simple. Si tienes un proyecto propio, puedes adaptar los ejercicios.

Setup del proyecto de ejemplo

# Crear proyecto
mkdir claude-hooks-practice
cd claude-hooks-practice
 
# Inicializar
npm init -y
git init
 
# Instalar dependencias
npm install --save-dev \
  typescript \
  prettier \
  eslint \
  @typescript-eslint/parser \
  @typescript-eslint/eslint-plugin \
  jest
 
# Crear estructura básica
mkdir -p src .claude/hooks

src/calculator.ts:

export function add(a: number, b: number): number {
  return a + b;
}
 
export function multiply(a: number, b: number): number {
  return a * b;
}

src/calculator.test.ts:

import { add, multiply } from './calculator';
 
describe('Calculator', () => {
  test('add sums two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });
 
  test('multiply multiplies two numbers', () => {
    expect(multiply(3, 4)).toBe(12);
  });
});

Ejercicios

Ejercicio 1: Hook de formateo automático (Básico)

Objetivo: Configurar Prettier para formatear automáticamente archivos TypeScript al editarlos.

Tareas:

  1. Crea .claude/settings.json con un hook PostToolUse
  2. El hook debe ejecutarse cuando se use Write o Edit
  3. Debe formatear solo archivos .ts y .tsx
  4. Timeout de 30 segundos

Archivo a crear: .claude/hooks/format.sh

Criterio de éxito:

  • Editar un archivo .ts formatea automáticamente
  • No intenta formatear archivos no-TypeScript
  • Se completa en <5 segundos
Ver solución

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

.claude/hooks/format.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
if [[ "$file_path" =~ \.(ts|tsx)$ ]]; then
  echo "Formateando: $file_path" >&2
  npx prettier --write "$file_path" 2>&1
fi
 
exit 0
chmod +x .claude/hooks/format.sh

Ejercicio 2: Logging de comandos bash (Intermedio)

Objetivo: Registrar todos los comandos bash que Claude ejecuta para auditoría.

Tareas:

  1. Crear hook PreToolUse para la herramienta Bash
  2. Extraer el comando y descripción del JSON de entrada
  3. Guardar en .claude/bash-log.txt con timestamp
  4. Formato: [YYYY-MM-DD HH:MM:SS] descripción: comando

Criterio de éxito:

  • Cada comando bash queda registrado
  • El log es legible y tiene timestamps
  • No afecta la ejecución de comandos
Ver solución

.claude/settings.json (añadir):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

.claude/hooks/log-bash.sh:

#!/usr/bin/env bash
set -euo pipefail
 
command=$(jq -r '.tool_input.command')
description=$(jq -r '.tool_input.description // "Sin descripción"')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
 
echo "[$timestamp] $description: $command" >> "$CLAUDE_PROJECT_DIR/.claude/bash-log.txt"
 
exit 0
chmod +x .claude/hooks/log-bash.sh

Probar:

# En sesión de Claude Code, pedir:
> Ejecuta ls -la
 
# Ver log:
cat .claude/bash-log.txt

Ejercicio 3: Tests automáticos relacionados (Intermedio)

Objetivo: Ejecutar automáticamente los tests relacionados con el archivo modificado.

Tareas:

  1. Crear hook PostToolUse que ejecute tests con Jest
  2. Solo ejecutar para archivos en src/ (no tests)
  3. Usar --findRelatedTests de Jest
  4. No fallar si no hay tests (usar --passWithNoTests)
  5. Timeout de 120 segundos

Criterio de éxito:

  • Modificar src/calculator.ts ejecuta sus tests automáticamente
  • Modificar archivos fuera de src/ no ejecuta tests
  • Modificar *.test.ts no ejecuta tests (evitar loops)
Ver solución

.claude/settings.json (añadir a PostToolUse existente):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format.sh",
            "timeout": 30
          },
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-related.sh",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

.claude/hooks/test-related.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo procesar archivos TypeScript en src/
if [[ ! "$file_path" =~ ^.*/src/.* ]] || [[ ! "$file_path" =~ \.ts$ ]]; then
  exit 0
fi
 
# No ejecutar tests para archivos de test
if [[ "$file_path" =~ \.test\.ts$ ]]; then
  echo "Archivo de test modificado, skipping ejecución" >&2
  exit 0
fi
 
echo "Ejecutando tests relacionados con: $file_path" >&2
 
npm test -- \
  --findRelatedTests "$file_path" \
  --passWithNoTests \
  2>&1 || {
    echo "⚠ Algunos tests fallaron" >&2
    exit 0  # No bloquear por tests fallidos
  }
 
echo "✓ Tests completados" >&2
exit 0
chmod +x .claude/hooks/test-related.sh

Ejercicio 4: Protección de archivos sensibles (Avanzado)

Objetivo: Bloquear modificaciones a archivos sensibles como .env o configuraciones.

Tareas:

  1. Crear hook PreToolUse para Read, Write y Edit
  2. Verificar si el archivo es sensible (.env, .git/, secrets/, etc.)
  3. Si es sensible, bloquear con exit code 2
  4. Mostrar mensaje claro al usuario

Criterio de éxito:

  • Intentar editar .env es bloqueado
  • Intentar leer .git/config es bloqueado
  • Archivos normales funcionan sin problemas
  • El mensaje de error es claro
Ver solución

.claude/settings.json (añadir a PreToolUse):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh",
            "timeout": 10
          }
        ]
      },
      {
        "matcher": "Read|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-sensitive.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

.claude/hooks/block-sensitive.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path // empty')
 
if [[ -z "$file_path" ]]; then
  exit 0
fi
 
# Lista de patrones prohibidos
sensitive_patterns=(
  '\.env'
  '\.env\.'
  '\.git/'
  '\.ssh/'
  'secrets/'
  'credentials'
  '\.key$'
  '\.pem$'
)
 
# Verificar cada patrón
for pattern in "${sensitive_patterns[@]}"; do
  if echo "$file_path" | grep -qE "$pattern"; then
    echo "❌ BLOQUEADO: Acceso denegado a archivo sensible" >&2
    echo "   Archivo: $file_path" >&2
    echo "   Razón: Coincide con patrón protegido: $pattern" >&2
    exit 2  # Bloquear operación
  fi
done
 
exit 0
chmod +x .claude/hooks/block-sensitive.sh

Probar:

# En sesión de Claude Code:
> Crea un archivo .env con contenido de prueba
 
# Debería ser bloqueado con mensaje de error

Ejercicio 5: Sistema completo con notificaciones (Avanzado)

Objetivo: Configurar un sistema completo que incluya formateo, linting, tests y notificaciones.

Tareas:

  1. Combinar todos los hooks anteriores
  2. Añadir hook Stop para notificar cuando Claude termina
  3. Añadir hook SessionStart para inicializar log de sesión
  4. Optimizar timeouts según operación
  5. Implementar cache en formateo

Criterio de éxito:

  • Al editar un archivo: formateo + tests automáticos
  • Al ejecutar comando bash: queda registrado
  • Cuando Claude termina: notificación desktop
  • Archivos sensibles están protegidos
  • Todo funciona rápido (<5s para formateo, <30s para tests)
Ver solución

.claude/settings.json (configuración completa):

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[$(date -Iseconds)] Sesión iniciada\" >> \"$CLAUDE_PROJECT_DIR/.claude/session.log\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/log-bash.sh",
            "timeout": 10
          }
        ]
      },
      {
        "matcher": "Read|Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-sensitive.sh",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-cached.sh",
            "timeout": 30
          },
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/test-related.sh",
            "timeout": 120
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/notify.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

.claude/hooks/format-cached.sh:

#!/usr/bin/env bash
set -euo pipefail
 
file_path=$(jq -r '.tool_input.file_path')
 
# Solo TypeScript
if [[ ! "$file_path" =~ \.(ts|tsx)$ ]]; then
  exit 0
fi
 
# Cache
cache_dir="$CLAUDE_PROJECT_DIR/.claude/cache"
mkdir -p "$cache_dir"
content_hash=$(md5sum "$file_path" | cut -d' ' -f1)
cache_key="$cache_dir/format.$(basename "$file_path").$content_hash"
 
if [[ -f "$cache_key" ]]; then
  echo "Cache hit, skipping format" >&2
  exit 0
fi
 
echo "Formateando: $file_path" >&2
npx prettier --write "$file_path" 2>&1
 
touch "$cache_key"
find "$cache_dir" -type f -mtime +7 -delete 2>/dev/null || true
 
exit 0

.claude/hooks/notify.sh:

#!/usr/bin/env bash
set -euo pipefail
 
# Detectar sistema operativo
if command -v notify-send > /dev/null; then
  notify-send "Claude Code" "Claude ha terminado de trabajar" --icon=dialog-information
elif command -v osascript > /dev/null; then
  osascript -e 'display notification "Claude ha terminado de trabajar" with title "Claude Code"'
else
  echo "✓ Claude ha terminado" >&2
fi
 
exit 0
chmod +x .claude/hooks/*.sh

Ejercicio 6: Monitoreo de performance (Desafío)

Objetivo: Implementar un wrapper que mida la duración de cada hook y alerte si son lentos.

Tareas:

  1. Crear performance-wrapper.sh que ejecute otros hooks midiendo tiempo
  2. Registrar duración en .claude/performance.log
  3. Alertar si un hook tarda >5 segundos
  4. Modificar settings.json para usar el wrapper

Criterio de éxito:

  • Cada hook queda registrado con su duración
  • Puedes ver estadísticas de performance
  • Recibes alertas si algo es lento
Ver solución

.claude/hooks/performance-wrapper.sh:

#!/usr/bin/env bash
set -euo pipefail
 
TARGET_HOOK="${1:-}"
THRESHOLD_SECONDS="${2:-5}"
 
if [[ -z "$TARGET_HOOK" ]]; then
  echo "Uso: performance-wrapper.sh <hook-script> [threshold]" >&2
  exit 1
fi
 
# Leer JSON de stdin
json_input=$(cat)
 
# Ejecutar hook con timing
start=$(date +%s.%N)
echo "$json_input" | "$TARGET_HOOK"
exit_code=$?
end=$(date +%s.%N)
 
# Calcular duración
duration=$(echo "$end - $start" | bc)
 
# Log de performance
perf_log="$CLAUDE_PROJECT_DIR/.claude/performance.log"
echo "$(date -Iseconds),$(basename "$TARGET_HOOK"),$duration,$exit_code" >> "$perf_log"
 
# Alertar si es lento
if (( $(echo "$duration > $THRESHOLD_SECONDS" | bc -l) )); then
  echo "⚠ Hook lento: $(basename "$TARGET_HOOK") tomó ${duration}s" >&2
fi
 
exit $exit_code

Modificar .claude/settings.json para usar wrapper:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/performance-wrapper.sh .claude/hooks/format-cached.sh 3",
            "timeout": 40
          },
          {
            "type": "command",
            "command": ".claude/hooks/performance-wrapper.sh .claude/hooks/test-related.sh 30",
            "timeout": 150
          }
        ]
      }
    ]
  }
}

Ver estadísticas:

# Ver hooks más lentos
cat .claude/performance.log | sort -t, -k3 -rn | head -5
 
# Ver promedio
awk -F, '{sum+=$3; count++} END {print "Promedio:", sum/count "s"}' .claude/performance.log

Verificación

Checklist final

Verifica que tu configuración cumple con:

  • Formateo automático funciona
  • Tests se ejecutan solo para archivos relevantes
  • Comandos bash quedan registrados
  • Archivos sensibles están protegidos
  • Recibes notificaciones cuando Claude termina
  • Todos los hooks terminan en tiempo razonable (<2 minutos)
  • Cache evita trabajo duplicado
  • Logs son legibles y útiles
  • No hay errores en modo verbose (claude --verbose)

Comandos de verificación

# Ver estructura
tree .claude/
 
# Ver configuración
cat .claude/settings.json | jq
 
# Ver permisos de scripts
ls -la .claude/hooks/
 
# Ver logs
tail .claude/bash-log.txt
tail .claude/session.log
tail .claude/performance.log
 
# Probar hook manualmente
echo '{"tool_input":{"file_path":"src/calculator.ts"},"tool_name":"Write"}' | \
  .claude/hooks/format.sh

Desafíos extra

Si quieres ir más allá:

  1. Multi-lenguaje: Adaptar hooks para soportar Python además de TypeScript
  2. Git integration: Auto-commit después de cambios (con mensaje descriptivo)
  3. Slack notifications: Notificar al equipo cuando completas tareas
  4. Security scanning: Integrar herramientas como TruffleHog o Bandit
  5. Code coverage: Monitorear cobertura de tests y alertar si baja
  6. Conditional execution: Ejecutar tests completos solo en branch main

Recursos

  • 5.1 ¿Qué son los hooks?
  • 5.2 Tipos de hooks
  • 5.3 Configuración de hooks
  • 5.4 Casos de uso avanzados
  • 5.5 Timeout de hooks

¡Felicidades! Has completado la práctica del Módulo 5. Ahora tienes un sistema completo de automatización con hooks que mejorará significativamente tu flujo de trabajo con Claude Code.