Stop Hooks
Stop Hooks verhindern, dass Claude Code einen Turn beendet, solange Tests fehlschlagen, Builds brechen oder Lint rot ist. Vier Enforcement-Muster plus Schutz vor Endlosschleifen.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
Problem: Claude schließt eine Antwort ab, aber die Arbeit ist eigentlich nicht fertig. Tests schlagen noch fehl. Dateien sind halb geschrieben. Du fragst "bist du fertig?" und bekommst ein Ja, während der Build rot ist.
Quick Win: Wirf diesen Stop Hook rein, und Claude kann den Turn nicht beenden, bis alle Tests grün sind:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/test-gate.py"
}
]
}
]
}
}#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
# CRITICAL: Prevent infinite loops
if input_data.get('stop_hook_active', False):
sys.exit(0)
# Run tests
result = subprocess.run(['npm', 'test'], capture_output=True, timeout=60)
if result.returncode != 0:
output = {
"decision": "block",
"reason": "Tests are failing. Fix them before completing."
}
print(json.dumps(output))
sys.exit(0)
sys.exit(0)Ab jetzt kann Claude einen Turn mit einer fehlschlagenden Test-Suite schlicht nicht mehr abschließen.
Wie der Stop Hook funktioniert
Jedes Mal wenn Claude eine Antwort beenden will, feuert dieser Hook. Drei Dinge können passieren:
- Stopp erlauben - Exit 0, und der Turn endet sauber.
- Stopp blockieren - Gib
{"decision": "block", "reason": "..."}zurück, und Claude macht weiter. - Validierungen ausführen - Starte Tests, Checks oder beliebige Scripts.
Das Payload
{
"session_id": "uuid-string",
"stop_hook_active": false,
"transcript_path": "/path/to/transcript.jsonl"
}Achte auf stop_hook_active. Ein true-Wert bedeutet, Claude befindet sich bereits in einem erzwungenen Weiterlauf-Zustand aus einem früheren Block. Übersiehst du dieses Flag, bekommst du eine unkontrollierbare Endlosschleife.
Muster 1: Test Gate
Hält den Turn offen, bis alle Tests bestehen:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
result = subprocess.run(
['npm', 'test', '--passWithNoTests'],
capture_output=True,
timeout=120
)
if result.returncode != 0:
# Extract last 10 lines of test output for context
stderr = result.stderr.decode()[-500:] if result.stderr else ""
print(json.dumps({
"decision": "block",
"reason": f"Tests failing. Output: {stderr}"
}))
sys.exit(0)
sys.exit(0)Muster 2: Build-Validierung
Blockiert, bis das Projekt kompiliert:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
result = subprocess.run(
['npm', 'run', 'build'],
capture_output=True,
timeout=180
)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "Build failed. Fix compilation errors before completing."
}))
sys.exit(0)
sys.exit(0)Muster 3: Lint-Check
Kein Verlassen des Turns mit Lint-Fehlern:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
result = subprocess.run(
['npx', 'eslint', 'src/', '--max-warnings=0'],
capture_output=True,
timeout=60
)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "Lint errors detected. Run eslint --fix or resolve manually."
}))
sys.exit(0)
sys.exit(0)Muster 4: Task-Completion-Marker
Den Turn an einem spezifischen Aufgaben-Flag festmachen:
#!/usr/bin/env python3
import json
import sys
from pathlib import Path
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
# Check for incomplete task marker
marker = Path('.claude/incomplete-task')
if marker.exists():
task_info = marker.read_text().strip()
print(json.dumps({
"decision": "block",
"reason": f"Task incomplete: {task_info}. Finish it before stopping."
}))
sys.exit(0)
sys.exit(0)Den Marker zu Beginn der Arbeit setzen:
echo "Implement user authentication" > .claude/incomplete-task
Ihn löschen, wenn der Job erledigt ist:
rm .claude/incomplete-task
Endlosschleifen verhindern
Hier ist, warum das stop_hook_active-Flag so wichtig ist. Ohne es passiert das:
Claude responds → Stop hook fires → "block" → Claude continues
↓
Claude responds → Stop hook fires → INFINITE LOOP (without flag check)Das Flag immer zuerst prüfen:
if input_data.get('stop_hook_active', False):
sys.exit(0) # Allow stopping, break the loopMehrere Checks kombinieren
Ein einziger Hook kann mehrere Gates hintereinanderschalten:
#!/usr/bin/env python3
import json
import sys
import subprocess
input_data = json.load(sys.stdin)
if input_data.get('stop_hook_active', False):
sys.exit(0)
checks = [
(['npm', 'run', 'lint'], "Lint errors"),
(['npm', 'run', 'typecheck'], "Type errors"),
(['npm', 'test'], "Test failures"),
]
for cmd, error_msg in checks:
result = subprocess.run(cmd, capture_output=True, timeout=120)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": f"{error_msg} detected. Fix before completing."
}))
sys.exit(0)
sys.exit(0)Wann Stop Hooks sinnvoll sind
Gute Anwendungsfälle:
- Auf eine grüne Test-Suite warten, bevor "Aufgabe erledigt"
- Sicherstellen, dass der Build noch kompiliert
- Lint- und Type-Fehler abfangen
- Jede eigene Regel dafür, was "fertig" für dich bedeutet
Schlechte Anwendungsfälle:
- Alles, was länger als das 60-Sekunden-Timeout läuft
- Checks, die ins Netz gehen und flakig sind
- Prompts, die eine menschliche Antwort brauchen (keine Interaktion hier)
Konfiguration
In .claude/settings.json einbinden:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/stop-validation.py"
}
]
}
]
}
}Mehrere Hooks können gleichzeitig, parallel laufen. Ein block von einem einzigen hält Claude am Laufen.
Debugging
Steckt in einer Schleife fest?
- Prüf nochmal, ob du
stop_hook_activeganz oben im Script liest - Loggen:
echo "stop_hook_active: $stop_hook_active" >> ~/.claude/stop-debug.log
Block kommt nicht an?
- Das JSON muss genau so aussehen:
{"decision": "block", "reason": "..."} - Exit Code 0 verwenden, nicht 2. Exit 2 ist für einen anderen Blockierungspfad.
Tests laufen zu lange?
- Hook-Timeout ist 60 Sekunden
- Nur eine kleinere Teilmenge ausführen, oder die Suite beschleunigen
Das "Ralph Wilgum"-Muster
Dieses Muster kommt aus der Community und nutzt Stop Hooks, um eine persistente Aufgaben-Schleife zu erzwingen:
- Einen Task-Marker zu Beginn der Session setzen
- Den Stop Hook blockieren lassen, solange der Marker vorhanden ist
- Claude muss den Marker als Beweis für den Abschluss löschen
- Kein versehentliches "Ich bin fertig" mehr, während Arbeit noch offen ist
Das Ergebnis: Claude wechselt von Best-Effort zu garantiertem Abschluss.
Nächste Schritte
- Den Hooks-Guide lesen, um jeden Hook-Typ kennenzulernen
- Context Recovery einrichten, damit Sessions Kompaktierung überstehen
- Skill Activation für automatisches Skill-Laden konfigurieren
- Permission Hooks für Auto-Approval-Flows anschauen
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.