Free

Eseguire test automaticamente con gli Hooks: Claude sa subito se il codice ha introdotto errori

Usa i PostToolUse Hooks sugli strumenti Write/Edit per avviare i test dopo ogni modifica — i fallimenti vengono restituiti a Claude per la correzione immediata.


Claude Code modifica il codice velocemente — ma non esegue i test da solo. Per impostazione predefinita, una volta scritto un file considera il lavoro finito. Senza un'istruzione esplicita, non sa se la modifica ha rotto qualcosa.

Un Hook PostToolUse risolve il problema: ogni volta che Claude scrive un file, i test vengono eseguiti automaticamente. In caso di fallimento, il risultato viene inviato direttamente a Claude, che lo corregge subito.


Come funziona

Claude Code modifica i file con due strumenti: Write (crea o sovrascrive) e Edit (modifiche parziali). Agganciare un Hook alla fase PostToolUse di questi strumenti avvia un'esecuzione dei test ogni volta che un file viene salvato su disco.

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

L'Hook riceve il JSON completo della chiamata allo strumento via stdin, incluso il percorso del file modificato. Usare quel percorso per decidere quali test eseguire è la chiave di questo approccio.

Regole del codice di uscita (PostToolUse):
- exit 0: test superati — Claude continua senza interruzioni
- exit 2: test falliti — il contenuto di stderr viene iniettato a Claude, che vede l'errore e tenta di correggerlo


Versione base: scegliere il comando in base al tipo di file

L'approccio più semplice: controllare l'estensione del file ed eseguire la suite di test corrispondente.

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

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

# Non avviare i test quando Claude modifica un file di test
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 "Test falliti (file modificato: $file):" >&2
  echo "$result" >&2
  exit 2
fi

Versione avanzata: puntare al file di test specifico

Eseguire l'intera suite a ogni modifica è lento. Usa il percorso del file modificato per individuare solo i test pertinenti.

In un progetto Python, src/auth/login.py corrisponde tipicamente 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 "Test falliti:" >&2
  echo "$result" >&2
  exit 2
fi

Per JavaScript/TypeScript, Jest supporta il filtro per pattern di nome file:

#!/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 "Test falliti:" >&2
  echo "$result" >&2
  exit 2
fi

Configurazione completa

Coprire sia Write che Edit, con protezione da timeout per evitare che test bloccati paralizzino 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

# Saltare i file di test stessi e i file non-codice
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 "⚠ Test falliti (file: $(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"
          }
        ]
      }
    ]
  }
}

Note importanti

Non avviare i test modificando file di test

Quando Claude modifica un file di test, quel test non è ancora completo. Eseguirlo in quel momento è prematuro. Questa riga di filtro è essenziale:

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

Write e Edit usano nomi di campo diversi in tool_input

Write usa file_path, Edit usa path. Gestire entrambi con un fallback:

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

Tenere sotto controllo i timeout

Suite lente combinate con modifiche frequenti possono bloccare la sessione. timeout 60 è un default ragionevole; adattalo alla durata reale dei test del progetto.

Livello progetto, non globale

Questo Hook appartiene alla configurazione a livello di progetto (.claude/settings.json). I comandi di test variano troppo tra progetti diversi. Per una versione globale, lo script deve rilevare il tipo di progetto dinamicamente.


Il risultato

Con questa configurazione, il ritmo di lavoro di Claude diventa:

  1. Modifica un file sorgente
  2. L'Hook esegue automaticamente i test pertinenti
  3. Test falliti → Claude vede l'output degli errori e continua a modificare finché non passano
  4. Test superati → silenzio, Claude passa al passo successivo

Niente più "esegui i test" ripetuto continuamente. Ogni modifica di Claude ha una rete di sicurezza di test.