Free

Ejecutar pruebas automáticamente con Hooks: Claude sabe al instante si algo se rompe

Usa PostToolUse Hooks en las herramientas Write/Edit para lanzar pruebas tras cada cambio de archivo — los fallos se devuelven a Claude para que los corrija de inmediato.


Claude Code edita código rápido, pero no ejecuta pruebas por sí solo. Por defecto, en cuanto escribe un archivo considera que terminó el trabajo — a menos que le indiques explícitamente que pruebe, no sabe si el cambio rompió algo.

Un Hook PostToolUse resuelve esto: cada vez que Claude escribe un archivo, las pruebas se ejecutan automáticamente. Si fallan, el resultado se devuelve directamente a Claude para que lo corrija de inmediato.


Cómo funciona

Claude Code modifica archivos usando dos herramientas: Write (crear o sobreescribir) y Edit (cambios parciales). Colocar un Hook en la fase PostToolUse de estas herramientas dispara una ejecución de pruebas cada vez que un archivo se guarda en disco.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/run-tests.sh"
          }
        ]
      }
    ]
  }
}

El Hook recibe el JSON completo de la llamada a la herramienta a través de stdin, incluyendo la ruta del archivo modificado. Usar esa ruta para decidir qué pruebas ejecutar es la clave de este enfoque.

Reglas de código de salida (PostToolUse):
- exit 0: pruebas superadas — Claude continúa sin interrupciones
- exit 2: pruebas fallidas — el contenido de stderr se inyecta de vuelta a Claude, que ve el error e intenta corregirlo


Versión básica: elegir comando según tipo de archivo

El enfoque más simple: comprobar la extensión del archivo y ejecutar el conjunto de pruebas correspondiente.

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" ]] && exit 0

# No ejecutar pruebas cuando Claude edita el propio archivo de pruebas
echo "$file" | grep -qE '(test|spec)\.' && exit 0

case "$file" in
  *.py)
    cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
    result=$(python -m pytest --tb=short -q 2>&1)
    ;;
  *.ts|*.js|*.tsx|*.jsx)
    cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
    result=$(npm test -- --passWithNoTests 2>&1)
    ;;
  *.go)
    dir=$(dirname "$file")
    result=$(go test "./$dir/..." 2>&1)
    ;;
  *.rb)
    cd "$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
    result=$(bundle exec rspec --format progress 2>&1)
    ;;
  *)
    exit 0
    ;;
esac

exit_code=$?

if [[ $exit_code -ne 0 ]]; then
  echo "Pruebas fallidas (archivo modificado: $file):" >&2
  echo "$result" >&2
  exit 2
fi

Versión avanzada: apuntar al archivo de prueba específico

Ejecutar toda la suite en cada cambio es lento. Usa la ruta del archivo modificado para localizar solo las pruebas relevantes.

En un proyecto Python, src/auth/login.py suele mapearse a tests/auth/test_login.py:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" || "$file" != *.py ]] && exit 0
echo "$file" | grep -qE '(test_|_test\.py)' && exit 0

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"

basename=$(basename "$file" .py)
test_file=$(find tests -name "test_${basename}.py" 2>/dev/null | head -1)

if [[ -n "$test_file" ]]; then
  result=$(python -m pytest "$test_file" --tb=short -q 2>&1)
else
  result=$(python -m pytest --tb=short -q 2>&1)
fi

if [[ $? -ne 0 ]]; then
  echo "Pruebas fallidas:" >&2
  echo "$result" >&2
  exit 2
fi

Para JavaScript/TypeScript, Jest admite filtrado por patrón de nombre de archivo:

#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" ]] && exit 0
echo "$file" | grep -qE '\.(test|spec)\.' && exit 0
echo "$file" | grep -qE '\.(ts|tsx|js|jsx)$' || exit 0

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"

basename=$(basename "$file" | sed 's/\.[^.]*$//')
result=$(npx jest --testPathPattern="$basename" --passWithNoTests 2>&1)

if [[ $? -ne 0 ]]; then
  echo "Pruebas fallidas:" >&2
  echo "$result" >&2
  exit 2
fi

Configuración completa

Cubrir tanto Write como Edit, con un límite de tiempo para evitar que pruebas colgadas bloqueen a Claude:

.claude/hooks/run-tests.sh:

#!/bin/bash
set -euo pipefail

input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

[[ -z "$file" ]] && exit 0

# Omitir archivos de prueba y archivos que no son código
echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0
echo "$file" | grep -qE '\.(py|ts|js|tsx|jsx|go|rb)$' || exit 0

root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0
cd "$root"

run_test() {
  timeout 60 "$@" 2>&1
}

case "$file" in
  *.py)
    result=$(run_test python -m pytest --tb=short -q)
    ;;
  *.ts|*.tsx|*.js|*.jsx)
    basename=$(basename "$file" | sed 's/\.[^.]*$//')
    result=$(run_test npx jest --testPathPattern="$basename" --passWithNoTests)
    ;;
  *.go)
    dir=$(dirname "$file")
    result=$(run_test go test "./$dir/...")
    ;;
  *.rb)
    result=$(run_test bundle exec rspec --format progress)
    ;;
  *)
    exit 0
    ;;
esac

if [[ $? -ne 0 ]]; then
  echo "⚠ Pruebas fallidas (archivo: $(basename "$file"))" >&2
  echo "" >&2
  echo "$result" >&2
  exit 2
fi

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/run-tests.sh"
          }
        ]
      }
    ]
  }
}

Consideraciones importantes

No disparar pruebas al editar archivos de prueba

Cuando Claude edita un archivo de prueba, ese test aún no está completo. Ejecutarlo en ese momento es prematuro. Esta línea de filtro es esencial:

echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0

Write y Edit usan nombres de campo distintos en tool_input

Write usa file_path, Edit usa path. Manejar ambos con un fallback:

file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')

Controlar el tiempo de espera

Suites lentas combinadas con ediciones frecuentes pueden congelar la sesión. timeout 60 es un valor razonable; ajústalo según la duración real de las pruebas del proyecto.

Configuración a nivel de proyecto, no global

Este Hook pertenece a la configuración a nivel de proyecto (.claude/settings.json). Los comandos de prueba varían demasiado entre proyectos. Si quieres una versión global, el script necesita detectar el tipo de proyecto dinámicamente.


El resultado

Con esta configuración, el ritmo de trabajo de Claude es:

  1. Edita un archivo fuente
  2. El Hook ejecuta automáticamente las pruebas relevantes
  3. Pruebas fallidas → Claude ve la salida del error y sigue editando hasta que pasen
  4. Pruebas superadas → silencio, Claude pasa al siguiente paso

Sin necesidad de decirle "ejecuta las pruebas" una y otra vez. Cada cambio de Claude tiene una red de seguridad de pruebas.