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.
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
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
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
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"
}
]
}
]
}
}
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.
Con questa configurazione, il ritmo di lavoro di Claude diventa:
Niente più "esegui i test" ripetuto continuamente. Ogni modifica di Claude ha una rete di sicurezza di test.