Free

Lancer les tests automatiquement avec les Hooks : Claude sait immédiatement si son code casse quelque chose

Utilisez les PostToolUse Hooks sur les outils Write/Edit pour déclencher les tests après chaque modification de fichier — les échecs sont renvoyés à Claude pour correction immédiate.


Claude Code modifie le code vite — mais ne lance pas les tests tout seul. Par défaut, dès qu'il écrit un fichier, il considère la tâche terminée. Sans instruction explicite, il ne sait pas si le changement a cassé quoi que ce soit.

Un Hook PostToolUse règle ce problème : chaque fois que Claude écrit un fichier, les tests se lancent automatiquement. En cas d'échec, le résultat est renvoyé directement à Claude, qui corrige immédiatement.


Comment ça fonctionne

Claude Code modifie les fichiers avec deux outils : Write (créer ou écraser) et Edit (modifications partielles). Accrocher un Hook à la phase PostToolUse de ces outils déclenche une exécution des tests à chaque écriture de fichier.

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

Le Hook reçoit le JSON complet de l'appel outil via stdin, y compris le chemin du fichier modifié. Utiliser ce chemin pour décider quels tests lancer est la clé de cette approche.

Règles de code de sortie (PostToolUse) :
- exit 0 : tests réussis — Claude continue sans interruption
- exit 2 : tests échoués — le contenu de stderr est injecté à Claude, qui voit l'erreur et tente de corriger


Version basique : choisir la commande selon le type de fichier

L'approche la plus simple : vérifier l'extension du fichier et lancer la suite de tests correspondante.

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

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

# Ne pas déclencher de tests quand Claude modifie un fichier de 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 "Tests échoués (fichier modifié : $file) :" >&2
  echo "$result" >&2
  exit 2
fi

Version avancée : cibler le fichier de test précis

Lancer toute la suite à chaque changement est trop lent. Utilisez le chemin du fichier modifié pour ne lancer que les tests concernés.

Dans un projet Python, src/auth/login.py correspond généralement à 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 "Tests échoués :" >&2
  echo "$result" >&2
  exit 2
fi

Pour JavaScript/TypeScript, Jest supporte le filtrage par pattern de nom de fichier :

#!/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 "Tests échoués :" >&2
  echo "$result" >&2
  exit 2
fi

Configuration complète

Couvrir Write et Edit, avec une protection par timeout pour éviter que des tests bloqués ne paralysent 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

# Ignorer les fichiers de test et les fichiers non-code
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 "⚠ Tests échoués (fichier : $(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"
          }
        ]
      }
    ]
  }
}

Points importants

Ne pas déclencher les tests lors de l'édition d'un fichier de test

Quand Claude modifie un fichier de test, ce test n'est pas encore terminé. Le lancer à ce moment est prématuré. Cette ligne de filtrage est essentielle :

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

Write et Edit utilisent des noms de champs différents dans tool_input

Write utilise file_path, Edit utilise path. Gérer les deux avec un fallback :

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

Maîtriser le timeout des tests

Des suites lentes combinées à des modifications fréquentes peuvent geler la session. timeout 60 est une valeur raisonnable ; ajustez selon la durée réelle des tests du projet.

Niveau projet, pas global

Ce Hook appartient à la configuration au niveau du projet (.claude/settings.json). Les commandes de test varient trop d'un projet à l'autre. Pour une version globale, le script doit détecter le type de projet dynamiquement.


Le résultat

Avec cette configuration, le rythme de travail de Claude devient :

  1. Modification d'un fichier source
  2. Le Hook lance automatiquement les tests concernés
  3. Tests échoués → Claude voit la sortie d'erreur et continue à modifier jusqu'à ce qu'ils passent
  4. Tests réussis → silence, Claude passe à l'étape suivante

Plus besoin de répéter « lance les tests ». Chaque modification de Claude dispose d'un filet de sécurité de tests.