PostToolUse Hooks auf Write/Edit-Tools setzen, um nach jeder Dateiänderung Tests auszulösen — Fehler werden direkt an Claude zurückgegeben, damit es sie sofort behebt.
Claude Code bearbeitet Code schnell — aber führt keine Tests aus. Standardmäßig gilt die Aufgabe als erledigt, sobald eine Datei geschrieben ist. Ohne explizite Anweisung weiß Claude nicht, ob die Änderung etwas kaputt gemacht hat.
Ein PostToolUse Hook löst das: Jedes Mal, wenn Claude eine Datei schreibt, laufen die Tests automatisch. Bei Fehlern wird das Ergebnis direkt an Claude zurückgegeben, das sofort mit der Korrektur beginnt.
Claude Code bearbeitet Dateien mit zwei Tools: Write (Erstellen oder Überschreiben) und Edit (Teiländerungen). Ein Hook in der PostToolUse-Phase dieser Tools startet bei jeder Dateiänderung automatisch einen Testlauf.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/run-tests.sh"
}
]
}
]
}
}
Der Hook empfängt den vollständigen JSON-Body des Tool-Aufrufs über stdin, inklusive des Pfads der geänderten Datei. Anhand dieses Pfads zu entscheiden, welche Tests laufen sollen, ist der Kern dieses Ansatzes.
Exit-Code-Regeln (PostToolUse):
- exit 0: Tests bestanden — Claude macht ohne Unterbrechung weiter
- exit 2: Tests fehlgeschlagen — der stderr-Inhalt wird an Claude zurückgegeben, das den Fehler sieht und versucht, ihn zu beheben
Der einfachste Ansatz: Dateiendung prüfen und das passende Test-Framework starten.
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[[ -z "$file" ]] && exit 0
# Keine Tests auslösen, wenn Claude eine Testdatei bearbeitet
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 fehlgeschlagen (geänderte Datei: $file):" >&2
echo "$result" >&2
exit 2
fi
Die gesamte Test-Suite bei jeder Änderung zu starten ist zu langsam. Nutze den Pfad der geänderten Datei, um nur die relevanten Tests zu finden.
In einem Python-Projekt entspricht src/auth/login.py typischerweise 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 fehlgeschlagen:" >&2
echo "$result" >&2
exit 2
fi
Für JavaScript/TypeScript unterstützt Jest das Filtern per Dateinamen-Pattern:
#!/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 fehlgeschlagen:" >&2
echo "$result" >&2
exit 2
fi
Write und Edit abdecken, mit Timeout-Schutz damit hängende Tests Claude nicht blockieren:
.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
# Testdateien selbst und Nicht-Code-Dateien überspringen
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 fehlgeschlagen (ausgelöst durch: $(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"
}
]
}
]
}
}
Keine Tests auslösen beim Bearbeiten von Testdateien
Wenn Claude eine Testdatei bearbeitet, ist der Test noch nicht fertig. Ihn jetzt zu starten ist verfrüht. Diese Filterzeile ist entscheidend:
echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0
Write und Edit nutzen unterschiedliche Feldnamen in tool_input
Write nutzt file_path, Edit nutzt path. Beide mit einem Fallback abdecken:
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
Timeouts im Griff behalten
Langsame Test-Suites kombiniert mit häufigen Änderungen können die Session einfrieren. timeout 60 ist ein sinnvoller Standard — an die tatsächliche Testdauer des Projekts anpassen.
Projektebene, nicht global
Dieser Hook gehört in die Projektkonfiguration (.claude/settings.json). Testbefehle variieren zu stark zwischen Projekten. Für eine globale Version muss das Skript den Projekttyp dynamisch erkennen.
Mit dieser Konfiguration sieht Claudes Arbeitsrhythmus so aus:
Kein wiederholtes „Führ mal die Tests aus". Jede Änderung von Claude hat ein Test-Sicherheitsnetz.