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

Automatizar validaciones con Hooks

Automatizar validaciones con Hooks

Descripción

En la cápsula anterior aprendiste la mecánica de los hooks: los 5 eventos, matchers, return codes, variables de entorno. Ahora viene la aplicación práctica: construir un pipeline de calidad automatizado que valida el trabajo de Claude Code en tiempo real.

La idea es simple pero poderosa: cada vez que Claude escribe código, una serie de validaciones se ejecutan automáticamente. Linting, formateo, type checking, tests. Si algo falla, Claude lo ve y lo corrige antes de que tú tengas que intervenir. Es como tener un CI/CD que corre en cada keystroke, no en cada push.

Esta cápsula cubre 5 patterns de validación, cómo encadenarlos en un pipeline, comparaciones con CI/CD y git hooks, y los pitfalls que debes evitar para que tus hooks no se conviertan en un cuello de botella.


El poder de las validaciones automáticas

El problema sin hooks

Sin hooks, el flujo de calidad depende de tu memoria:

Claude escribe archivo
  → Tú: "Ejecuta eslint"
  → Claude ejecuta eslint → 3 errores
  → Tú: "Corrígelos"
  → Claude corrige
  → Tú: "Ahora ejecuta prettier"
  → Claude ejecuta prettier
  → Tú: "Ejecuta los tests"
  → Claude ejecuta tests → 1 falla
  → Tú: "Corrige el test"
  → ...

Son 6-8 interacciones manuales por cada archivo. Si Claude modifica 5 archivos en una tarea, son 30-40 interacciones solo de validación.

El flujo con hooks

Claude escribe archivo
  → [Hook PostToolUse] Prettier formatea automáticamente
  → [Hook PostToolUse] ESLint valida → errores visibles para Claude
  → Claude ve los errores y los corrige
  → [Hook PostToolUse] ESLint valida de nuevo → 0 errores
  → [Hook PostToolUse] Tests relacionados pasan
  → Siguiente archivo...

0 interacciones manuales de validación. Claude se auto-corrige basándose en el feedback de los hooks.

La clave: feedback loop automático

Lo más poderoso de los hooks no es que ejecutan validaciones — es que crean un feedback loop. Claude ve el output del hook, interpreta los errores, y los corrige en la siguiente acción. Es auto-corrección en tiempo real.

┌─────────┐     ┌──────────┐     ┌───────────┐
│ Claude   │────▶│  Hook    │────▶│  Output   │
│ escribe  │     │ valida   │     │  (errores │
│          │     │          │     │   o ✅)   │
└─────────┘     └──────────┘     └─────┬─────┘
      ▲                                │
      │         FEEDBACK LOOP          │
      └────────────────────────────────┘
      Claude lee el output y corrige

Pattern 1: Lint en cada escritura

El pattern más común y más impactante. Cada vez que Claude escribe un archivo, el linter se ejecuta automáticamente.

Configuración básica

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/lint-on-write.sh"
      }
    ]
  }
}

Script del hook

#!/bin/bash
# scripts/hooks/lint-on-write.sh

FILE="$CLAUDE_FILE_PATH"

case "$FILE" in
    *.ts|*.tsx)
        npx eslint "$FILE" --quiet 2>&1
        ;;
    *.js|*.jsx)
        npx eslint "$FILE" --quiet 2>&1
        ;;
    *.py)
        ruff check "$FILE" 2>&1
        ;;
    *.css|*.scss)
        npx stylelint "$FILE" --quiet 2>&1
        ;;
esac

exit 0

Por qué exit 0 y no el exit code del linter

En PostToolUse, el archivo ya está escrito. Retornar un código de error no "deshace" la escritura — solo informa a Claude que algo está mal. Claude ve el output del linter y puede corregir en su siguiente acción. Por eso usamos exit 0: queremos que Claude vea los errores como información, no como un bloqueo.

Con auto-fix

Si prefieres que el linter corrija automáticamente lo que pueda:

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

case "$FILE" in
    *.ts|*.tsx|*.js|*.jsx)
        npx eslint "$FILE" --fix --quiet 2>&1
        ;;
    *.py)
        ruff check "$FILE" --fix 2>&1
        ;;
esac

exit 0

--fix corrige problemas automáticos (espacios, imports, etc.). Los problemas lógicos se reportan para que Claude los resuelva.


Pattern 2: Format automático

Prettier (o tu formatter) se ejecuta automáticamente después de cada escritura. Garantiza que todo el código generado por Claude sigue el estilo del proyecto.

Configuración

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

¿Format antes o después del lint?

El orden importa:

Opción A: Format → Lint
  Prettier formatea → ESLint valida el código formateado
  ✅ Menos conflictos (Prettier y ESLint no se contradicen)
  
Opción B: Lint → Format
  ESLint valida → Prettier formatea
  ❌ ESLint puede reportar errores de formato que Prettier va a arreglar de todos modos

Recomendación: Format primero, lint después:

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

Pattern 3: Tests después de implementación

Ejecutar tests relacionados automáticamente después de que Claude modifica un archivo de código.

El desafío: ¿qué tests ejecutar?

Ejecutar toda la suite en cada escritura es demasiado lento. La clave es ejecutar solo los tests relacionados con el archivo modificado.

Estrategia: mapeo archivo → test

#!/bin/bash
# scripts/hooks/run-related-tests.sh

FILE="$CLAUDE_FILE_PATH"

# Solo archivos de source (no configs, no docs)
if [[ ! "$FILE" == src/* ]] && [[ ! "$FILE" == app/* ]]; then
    exit 0
fi

# Si es un archivo de test, ejecutarlo directamente
if [[ "$FILE" == *test* ]] || [[ "$FILE" == *spec* ]]; then
    echo "Ejecutando test: $FILE"
    npx vitest run "$FILE" --reporter=dot 2>&1
    exit 0
fi

# Buscar test correspondiente
BASENAME=$(basename "$FILE" | sed 's/\.[^.]*$//')
TEST_FILES=$(find . -name "*${BASENAME}*test*" -o -name "*${BASENAME}*spec*" -o -name "test_${BASENAME}*" 2>/dev/null | head -3)

if [ -n "$TEST_FILES" ]; then
    echo "Tests relacionados encontrados:"
    for TEST in $TEST_FILES; do
        echo "  → $TEST"
        npx vitest run "$TEST" --reporter=dot 2>&1
    done
else
    echo "ℹ️  No se encontraron tests para $BASENAME"
fi

exit 0

Para proyectos Python

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

if [[ "$FILE" == *.py ]]; then
    BASENAME=$(basename "$FILE" .py)
    TEST_FILE="tests/test_${BASENAME}.py"
    
    if [ -f "$TEST_FILE" ]; then
        echo "Ejecutando: pytest $TEST_FILE -v"
        pytest "$TEST_FILE" -v --tb=short 2>&1
    fi
fi

exit 0

Cuándo ejecutar la suite completa

La suite completa es más apropiada para Stop que para PostToolUse:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/run-related-tests.sh"
      }
    ],
    "Stop": [
      {
        "command": "npm test --silent 2>&1 | tail -5"
      }
    ]
  }
}

Tests relacionados en cada escritura. Suite completa al final de la tarea.


Pattern 4: Validación de seguridad

Escanear código en busca de secrets, credenciales, y vulnerabilidades antes de permitir que se escriba.

PreToolUse para bloquear escritura insegura

#!/bin/bash
# scripts/hooks/security-check.sh

FILE="$CLAUDE_FILE_PATH"
INPUT="$CLAUDE_TOOL_INPUT"

# Detectar posibles secrets en el contenido que se va a escribir
PATTERNS=(
    "password\s*=\s*['\"]"
    "api_key\s*=\s*['\"]"
    "secret\s*=\s*['\"]"
    "token\s*=\s*['\"][a-zA-Z0-9]"
    "AWS_ACCESS_KEY"
    "PRIVATE_KEY"
    "-----BEGIN RSA"
    "-----BEGIN OPENSSH"
)

for PATTERN in "${PATTERNS[@]}"; do
    if echo "$INPUT" | grep -iEq "$PATTERN"; then
        echo "⛔ SEGURIDAD: Se detectó un posible secret/credencial"
        echo "   Patrón: $PATTERN"
        echo "   Archivo: $FILE"
        echo ""
        echo "   Usa variables de entorno en lugar de hardcodear secrets."
        echo "   Ejemplo: os.environ['API_KEY'] o process.env.API_KEY"
        exit 1
    fi
done

exit 0

Configuración

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/security-check.sh"
      }
    ]
  }
}

Si Claude intenta escribir un archivo con password = "admin123", el hook lo bloquea y le sugiere usar variables de entorno. Claude ve el mensaje y reescribe usando os.environ['PASSWORD'].

PostToolUse para auditoría

Si prefieres no bloquear sino alertar:

#!/bin/bash
# scripts/hooks/security-audit.sh

FILE="$CLAUDE_FILE_PATH"

if [ -f "$FILE" ]; then
    ISSUES=$(grep -inE "(password|api_key|secret|token)\s*=\s*['\"]" "$FILE" 2>/dev/null)
    if [ -n "$ISSUES" ]; then
        echo "⚠️  ALERTA DE SEGURIDAD en $FILE:"
        echo "$ISSUES"
        echo ""
        echo "Considera usar variables de entorno para estos valores."
    fi
fi

exit 0

Pattern 5: Type checking

Para proyectos TypeScript, ejecutar el compilador después de cada escritura detecta errores de tipos en tiempo real.

Configuración

#!/bin/bash
# scripts/hooks/type-check.sh

FILE="$CLAUDE_FILE_PATH"

if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
    # Type check solo del archivo modificado (más rápido que todo el proyecto)
    ERRORS=$(npx tsc --noEmit 2>&1 | grep "$FILE")
    
    if [ -n "$ERRORS" ]; then
        echo "⚠️  Errores de tipo en $FILE:"
        echo "$ERRORS"
    fi
fi

exit 0

Consideración de performance

tsc --noEmit puede tardar varios segundos en proyectos grandes. Opciones:

  1. Ejecutar solo en Stop (no en cada escritura):
{
  "hooks": {
    "Stop": [
      {
        "command": "npx tsc --noEmit 2>&1 | head -20"
      }
    ]
  }
}
  1. Usar tsc --noEmit con --incremental para cachear resultados:
npx tsc --noEmit --incremental 2>&1 | grep "$FILE"
  1. Aceptar el trade-off si el proyecto es pequeño (< 100 archivos):
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx tsc --noEmit 2>&1 | grep $CLAUDE_FILE_PATH || true"
      }
    ]
  }
}

Construir un pipeline completo

El pipeline recomendado

Para un proyecto TypeScript/React típico, este es el pipeline óptimo:

PostToolUse (Write):
  1. Prettier (format)      → ~100ms por archivo
  2. ESLint (lint)           → ~200ms por archivo
  3. Tests relacionados      → ~1-3s por archivo

Stop:
  4. tsc --noEmit (types)    → ~3-5s total
  5. Suite completa de tests → variable

Configuración del pipeline

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/security-check.sh"
      },
      {
        "matcher": "Execute",
        "command": "./scripts/hooks/safety-gate.sh"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/format.sh"
      },
      {
        "matcher": "Write",
        "command": "./scripts/hooks/lint.sh"
      },
      {
        "matcher": "Write",
        "command": "./scripts/hooks/related-tests.sh"
      }
    ],
    "Stop": [
      {
        "command": "./scripts/hooks/type-check.sh"
      },
      {
        "command": "./scripts/hooks/full-test-suite.sh"
      }
    ]
  }
}

Performance del pipeline

HookTiempo típicoFrecuencia
Prettier~100msPor archivo escrito
ESLint~200msPor archivo escrito
Tests relacionados~1-3sPor archivo escrito
tsc --noEmit~3-5sPor tarea completada
Suite completa~10-30sPor tarea completada

Para una tarea donde Claude escribe 5 archivos, el overhead total es:

  • PostToolUse: 5 × (~300ms + ~2s) ≈ ~11.5s distribuidos durante la tarea
  • Stop: ~5s + ~20s = ~25s al final de la tarea

Esto es aceptable. Si supera estos tiempos, optimiza los hooks más lentos.

Optimización: Conditional hooks con if (marzo 2026)

Feature reciente: los hooks pueden declarar una condición con if usando permission rule syntax. El hook solo se ejecuta cuando el tool call matchea la condición, reduciendo overhead en sesiones con muchas operaciones:

{
  "hooks": {
    "PreToolUse": [{
      "hooks": [{
        "if": "Bash(git commit *)",
        "type": "command",
        "command": ".claude/hooks/lint-staged.sh"
      }]
    }]
  }
}

Con if: "Bash(git commit *)", el lint-staged solo corre en git commits — no en cada bash call. Útil cuando tienes hooks específicos a ciertas operaciones.


Comparaciones y decisiones

Hooks vs CI/CD

AspectoHooks de Claude CodeCI/CD (GitHub Actions, etc.)
Cuándo se ejecutaDurante el desarrollo, en tiempo realDespués del push, en remoto
FeedbackInmediato (Claude corrige al instante)Diferido (minutos después del push)
Qué validaCada archivo individualTodo el proyecto
Costo de errorBajo (se corrige antes de commit)Alto (ya está en el repo)
PerformanceDebe ser rápido (< 5s por hook)Puede tardar minutos

No compiten — son capas complementarias:

Capa 1: Hooks de Claude Code    → Catch errores durante desarrollo
Capa 2: Git pre-commit hooks    → Catch errores al commitear
Capa 3: CI/CD                   → Catch errores al pushear/mergear

Cuanto antes detectas un error, más barato es corregirlo. Los hooks de Claude Code son la primera línea de defensa.

Hooks de Claude Code vs pre-commit (git)

AspectoClaude Code HooksGit pre-commit
GranularidadPor archivo, por herramientaPor commit
Agent-aware✅ Sabe qué herramienta usó Claude❌ No sabe quién hizo el cambio
Feedback a Claude✅ Claude ve y corrige❌ No interactúa con Claude
TimingEn tiempo real (cada escritura)Al commitear

Cuándo usar solo hooks ligeros

Si tu proyecto es grande y los hooks son lentos, reduce al mínimo:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "npx prettier --write $CLAUDE_FILE_PATH 2>/dev/null || true"
      }
    ],
    "Stop": [
      {
        "command": "npm test --silent 2>&1 | tail -5"
      }
    ]
  }
}

Solo format automático + tests al final. Sin lint ni type check en hooks (dejarlo para CI/CD).


Patterns avanzados

Pattern: Hook condicional por branch

#!/bin/bash
# Solo ejecutar validaciones estrictas en main/develop

BRANCH=$(git branch --show-current 2>/dev/null)

case "$BRANCH" in
    main|develop|staging)
        # Validación estricta
        npx eslint "$CLAUDE_FILE_PATH" --max-warnings=0 2>&1
        ;;
    *)
        # Validación relajada (solo errores, no warnings)
        npx eslint "$CLAUDE_FILE_PATH" --quiet 2>&1 || true
        ;;
esac

exit 0

Pattern: Hook con cache de resultados

#!/bin/bash
# Cachear resultados de lint para evitar re-ejecutar en archivos sin cambios

FILE="$CLAUDE_FILE_PATH"
CACHE_DIR=".claude/cache/lint"
mkdir -p "$CACHE_DIR"

FILE_HASH=$(md5sum "$FILE" 2>/dev/null | cut -d' ' -f1)
CACHE_FILE="$CACHE_DIR/$(echo $FILE | tr '/' '_').hash"

if [ -f "$CACHE_FILE" ] && [ "$(cat $CACHE_FILE)" = "$FILE_HASH" ]; then
    exit 0
fi

npx eslint "$FILE" --quiet 2>&1
RESULT=$?

if [ $RESULT -eq 0 ]; then
    echo "$FILE_HASH" > "$CACHE_FILE"
fi

exit 0

Pattern: Notificación al terminar tarea larga

#!/bin/bash
# scripts/hooks/notify-done.sh

MODIFIED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')

if [ "$MODIFIED" -gt 5 ]; then
    echo "📋 Tarea grande completada: $MODIFIED archivos modificados"
    echo "Archivos:"
    git diff --name-only 2>/dev/null | head -10
    
    if [ "$MODIFIED" -gt 10 ]; then
        echo "  ... y $(($MODIFIED - 10)) más"
    fi
fi

Pitfalls y edge cases

Pitfall 1: Hooks demasiado lentos

Síntoma: Claude tarda mucho entre acciones. La sesión se siente lenta.

Diagnóstico: Mide el tiempo de tus hooks:

time ./scripts/hooks/post-write-validate.sh

Solución: Cualquier hook PostToolUse debe completarse en < 3 segundos. Si tarda más:

  • Mueve la validación a Stop
  • Usa --incremental o caching
  • Reduce el scope (solo el archivo modificado, no todo el proyecto)

Pitfall 2: Hooks demasiado estrictos

Síntoma: Claude no puede completar tareas porque los PreToolUse bloquean operaciones legítimas.

Ejemplo problemático:

# Bloquea CUALQUIER archivo que contenga "TODO"
if grep -q "TODO" "$FILE"; then
    echo "No se permiten TODOs en el código"
    exit 1
fi

Solución: Los PreToolUse que bloquean deben ser para verdaderos riesgos de seguridad, no para preferencias de estilo. Usa PostToolUse para warnings:

# PostToolUse: informa pero no bloquea
TODO_COUNT=$(grep -c "TODO" "$FILE" 2>/dev/null || echo "0")
if [ "$TODO_COUNT" -gt 0 ]; then
    echo "ℹ️  $TODO_COUNT TODOs encontrados en $FILE"
fi
exit 0

Pitfall 3: No testear los hooks

El error: Configurar hooks y asumir que funcionan.

La solución: Testea cada hook manualmente antes de ponerlo en producción:

# Simular variables de entorno
CLAUDE_FILE_PATH="src/components/Test.tsx" \
CLAUDE_TOOL_NAME="Write" \
./scripts/hooks/post-write-validate.sh

Pitfall 4: Hooks que escriben archivos (loop potencial)

El error: Un PostToolUse hook que modifica archivos (ej. Prettier con --write).

El riesgo: Si Claude Code trata la modificación de Prettier como una nueva escritura, se dispara el hook de nuevo.

La realidad: Claude Code tiene protecciones contra esto — los cambios hechos por hooks no disparan nuevos hooks. Pero es buena práctica diseñar hooks idempotentes (ejecutar dos veces produce el mismo resultado que una).

Pitfall 5: Ignorar el output de los hooks

El error: Configurar hooks pero no revisar si Claude está actuando sobre su feedback.

La solución: Verifica que Claude corrige los errores que los hooks reportan. Si Claude ignora consistentemente el output de un hook, puede ser que:

  • El output es demasiado verbose (Claude pierde los errores en el ruido)
  • El formato no es claro (Claude no entiende qué corregir)
  • El hook reporta demasiados false positives (Claude aprende a ignorarlo)

Haz el output conciso y accionable.


Ejemplo completo integrado

Escenario: configuras un pipeline de validación completo para un proyecto FastAPI + React (monorepo).

Estructura

scripts/hooks/
├── security-check.sh
├── safety-gate.sh
├── format.sh
├── lint.sh
├── related-tests.sh
├── type-check.sh
└── task-summary.sh

settings.json

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Write", "command": "./scripts/hooks/security-check.sh" },
      { "matcher": "Execute", "command": "./scripts/hooks/safety-gate.sh" }
    ],
    "PostToolUse": [
      { "matcher": "Write", "command": "./scripts/hooks/format.sh" },
      { "matcher": "Write", "command": "./scripts/hooks/lint.sh" },
      { "matcher": "Write", "command": "./scripts/hooks/related-tests.sh" }
    ],
    "Stop": [
      { "command": "./scripts/hooks/type-check.sh" },
      { "command": "./scripts/hooks/task-summary.sh" }
    ]
  }
}

Script multi-lenguaje: format.sh

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

case "$FILE" in
    *.ts|*.tsx|*.js|*.jsx|*.css|*.scss|*.json|*.md)
        npx prettier --write "$FILE" --log-level=warn 2>/dev/null || true
        ;;
    *.py)
        ruff format "$FILE" 2>/dev/null || true
        ;;
esac

exit 0

Script multi-lenguaje: lint.sh

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

case "$FILE" in
    *.ts|*.tsx|*.js|*.jsx)
        ERRORS=$(npx eslint "$FILE" --quiet 2>&1)
        if [ -n "$ERRORS" ]; then
            echo "$ERRORS"
        fi
        ;;
    *.py)
        ERRORS=$(ruff check "$FILE" 2>&1)
        if [ -n "$ERRORS" ]; then
            echo "$ERRORS"
        fi
        ;;
esac

exit 0

task-summary.sh

#!/bin/bash
echo ""
echo "=== Resumen de tarea ==="

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

echo "Archivos modificados: $MODIFIED"
echo "Archivos nuevos: $NEW"

# Type check (TypeScript)
if [ -f "tsconfig.json" ]; then
    TSC_ERRORS=$(npx tsc --noEmit 2>&1 | grep -c "error TS" || echo "0")
    if [ "$TSC_ERRORS" -gt 0 ]; then
        echo "TypeScript: ❌ $TSC_ERRORS errores"
    else
        echo "TypeScript: ✅ Sin errores"
    fi
fi

# Tests
TEST_OUTPUT=$(npm test --silent 2>&1)
if [ $? -eq 0 ]; then
    echo "Tests: ✅ Passing"
else
    echo "Tests: ❌ Failing"
    echo "$TEST_OUTPUT" | tail -5
fi

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

Ejercicios prácticos

Ejercicio 1: Básico — Lint automático

Configura un hook que ejecute el linter de tu proyecto automáticamente después de cada escritura.

Requisitos:

  • Detecta el tipo de archivo (JS/TS/Python/CSS)
  • Ejecuta el linter correcto para cada tipo
  • No bloquea (informativo)
  • Output limpio (solo errores, no warnings)
Solución

Usa el script lint.sh del ejemplo integrado. Adapta los comandos de lint a los que usa tu proyecto (ESLint, Ruff, Stylelint, etc.).

Configura en .claude/settings.json:

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

Verifica: pide a Claude que cree un archivo con un error de lint intencional y confirma que el hook lo reporta.

Ejercicio 2: Básico — Format + Lint pipeline

Configura un pipeline de 2 pasos: primero format, luego lint.

Requisitos:

  • Prettier (o tu formatter) primero
  • Linter después
  • Ambos solo en archivos de código (no en configs, no en markdowns)
Solución
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "command": "./scripts/hooks/format.sh"
      },
      {
        "matcher": "Write",
        "command": "./scripts/hooks/lint.sh"
      }
    ]
  }
}

El orden es importante: format antes de lint para evitar que el linter reporte problemas de formato que Prettier va a corregir.

Ejercicio 3: Intermedio — Security check

Crea un hook PreToolUse que bloquee la escritura de archivos que contengan secrets hardcodeados.

Requisitos:

  • Detectar al menos 5 patterns (password, api_key, secret, token, private_key)
  • Bloquear la escritura (exit 1)
  • Mensaje de error claro con sugerencia de cómo hacerlo correctamente
  • No bloquear archivos .env.example (que es legítimo que tengan placeholders)
Solución

Adapta el script security-check.sh del Pattern 4. Agrega la excepción:

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

# No validar archivos de ejemplo
if [[ "$FILE" == *.example ]] || [[ "$FILE" == *.sample ]]; then
    exit 0
fi

PATTERNS=(
    "password\s*=\s*['\"][^{]"
    "api_key\s*=\s*['\"][^{]"
    "secret\s*=\s*['\"][^{]"
    "token\s*=\s*['\"][a-zA-Z0-9]"
    "AWS_ACCESS_KEY_ID\s*=\s*['\"]AK"
    "PRIVATE.KEY\s*=\s*['\"]"
)

for PATTERN in "${PATTERNS[@]}"; do
    if echo "$INPUT" | grep -iEq "$PATTERN"; then
        echo "⛔ Secret detectado. Usa variables de entorno."
        echo "   Ejemplo: process.env.API_KEY o os.environ['API_KEY']"
        exit 1
    fi
done

exit 0

Ejercicio 4: Intermedio — Tests relacionados

Crea un hook que ejecute automáticamente los tests relacionados con el archivo que Claude acaba de escribir.

Requisitos:

  • Mapear archivo de source → archivo de test
  • Ejecutar solo los tests del archivo modificado (no toda la suite)
  • Si no hay test correspondiente, mostrar un mensaje informativo
  • Si el archivo ES un test, ejecutarlo directamente
Solución

Usa el script run-related-tests.sh del Pattern 3. Adapta las convenciones de naming a tu proyecto:

  • src/foo.ts → tests/foo.test.ts o src/__tests__/foo.test.ts
  • app/services/foo.py → tests/test_foo.py

Testea manualmente con:

CLAUDE_FILE_PATH="src/components/Button.tsx" ./scripts/hooks/related-tests.sh

Ejercicio 5: Avanzado — Pipeline completo con los 5 eventos

Configura los 5 eventos reales de hooks con un pipeline de validación profesional.

Requisitos:

  • PreToolUse: security check (Write) + safety gate (Execute)
  • PostToolUse: format + lint + tests relacionados
  • Stop: type check + resumen con estado
  • Notification: alerta al completar tareas largas
Solución

Usa la configuración completa del ejemplo integrado de esta cápsula. Crea cada script, hazlo ejecutable, y verifica:

  1. Pide a Claude que cree un archivo → ¿format + lint se ejecutan?
  2. Pide a Claude que escriba un secret → ¿el security check lo bloquea?
  3. Cuando Claude termina una tarea → ¿ves el resumen del hook Stop?
  4. ¿Se ejecuta el hook de Notification cuando Claude envía notificaciones?

Resumen

Lo que aprendiste en esta cápsula:

  • Hooks crean un feedback loop: Claude ve errores → Claude corrige → hooks validan de nuevo
  • 5 patterns de validación: lint, format, tests, seguridad, type checking
  • El orden importa: format → lint → tests (en PostToolUse), suite completa (en Stop)
  • Performance: PostToolUse hooks deben ser rápidos (< 3s). Validaciones pesadas van en Stop
  • Hooks vs CI/CD: capas complementarias. Hooks = primera línea de defensa (en desarrollo). CI/CD = segunda línea (en push)
  • Pitfalls clave: hooks lentos, hooks demasiado estrictos, no testear los hooks, ignorar el output

Siguiente cápsula: 05 - Permission system — cómo controlar qué puede y qué no puede hacer Claude Code en tu proyecto.


Recursos adicionales

Documentación oficial

  • Hooks — Referencia completa de hooks y lifecycle events
  • Settings — Configuración de hooks en diferentes scopes
  • Best Practices — Recomendaciones para automatización

Herramientas de validación

  • ESLint — Linter para JavaScript/TypeScript
  • Prettier — Formatter de código
  • Ruff — Linter y formatter para Python (extremadamente rápido)

Complementarios