Stop Hooks
Les stop hooks empêchent Claude Code de terminer un tour tant que les tests échouent, le build est cassé, ou le lint est rouge. Quatre patterns d'application plus des protections anti-boucle infinie.
Arrêtez de configurer. Commencez à construire.
Templates SaaS avec orchestration IA.
Problème : Claude termine une réponse, mais le travail n'est pas vraiment fini. Les tests échouent encore. Des fichiers sont à moitié écrits. Tu demandes "tu as terminé ?" et tu obtiens un oui, pendant que le build est rouge.
Solution rapide : Ajoute ce Stop hook et Claude ne peut plus terminer le tour tant que les tests ne sont pas au vert :
{
"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)À partir de là, Claude ne peut littéralement plus clore un tour avec une suite en échec.
Comment fonctionne le Stop Hook
Chaque fois que Claude veut terminer une réponse, ce hook se déclenche. Trois choses peuvent se passer :
- Autoriser l'arrêt - Exit 0, et le tour se termine proprement.
- Bloquer l'arrêt - Retourne
{"decision": "block", "reason": "..."}, et Claude continue. - Lancer des validations - Exécute des tests, des vérifications, ou n'importe quel script.
Le payload
{
"session_id": "uuid-string",
"stop_hook_active": false,
"transcript_path": "/path/to/transcript.jsonl"
}Fais attention à stop_hook_active. Une valeur true signifie que Claude est déjà dans un état de continuation forcée suite à un blocage précédent. Manquer ce flag et tu te retrouves dans une boucle infinie.
Pattern 1 : Test Gate
Garde le tour ouvert jusqu'à ce que tous les tests passent :
#!/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)Pattern 2 : Validation du build
Bloque tant que le projet ne compile pas :
#!/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)Pattern 3 : Vérification du lint
Pas question de quitter le tour avec des erreurs de lint :
#!/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)Pattern 4 : Marqueur de complétion de tâche
Conditionne le tour à un flag de tâche spécifique :
#!/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)Dépose le marqueur au début du travail :
echo "Implement user authentication" > .claude/incomplete-task
Supprime-le quand c'est terminé :
rm .claude/incomplete-task
Éviter les boucles infinies
Voilà pourquoi le flag stop_hook_active est important. Sans lui, tu obtiens ça :
Claude responds → Stop hook fires → "block" → Claude continues
↓
Claude responds → Stop hook fires → INFINITE LOOP (without flag check)Vérifie toujours le flag en premier :
if input_data.get('stop_hook_active', False):
sys.exit(0) # Allow stopping, break the loopCombiner plusieurs vérifications
Un seul hook peut enchaîner plusieurs contrôles :
#!/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)Quand utiliser les Stop Hooks
Bons cas d'usage :
- Conditionner à une suite de tests verte avant "tâche terminée"
- S'assurer que le build compile toujours
- Attraper les erreurs de lint et de types
- Toute règle personnalisée sur ce que "terminé" signifie pour toi
Mauvais cas d'usage :
- Tout ce qui dépasse le timeout de 60 secondes
- Les vérifications réseau qui flanchent parfois
- Les prompts qui attendent une réponse humaine (pas d'interaction ici)
Configuration
Branche-le dans .claude/settings.json :
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/stop-validation.py"
}
]
}
]
}
}Plusieurs hooks peuvent tourner en même temps, en parallèle. Un block de l'un d'eux suffit à maintenir Claude en activité.
Débogage
Bloqué dans une boucle ?
- Vérifie bien que tu lis
stop_hook_activeau début du script - Logge-le :
echo "stop_hook_active: $stop_hook_active" >> ~/.claude/stop-debug.log
Le block ne fonctionne pas ?
- Le JSON doit ressembler à
{"decision": "block", "reason": "..."} - Utilise le code de sortie 0, pas 2. Le code 2 correspond à un autre chemin de blocage.
Les tests tournent trop longtemps ?
- Le timeout du hook est de 60 secondes
- Lance un sous-ensemble plus petit, ou accélère la suite
Le pattern "Ralph Wilgum"
Ça vient d'une technique communautaire qui utilise les Stop hooks pour forcer une boucle de tâche persistante :
- Dépose un marqueur de tâche au début de la session
- Le Stop hook bloque tant que le marqueur est là
- Tu exiges que Claude supprime le marqueur comme preuve de complétion
- Plus de "j'ai terminé" accidentel avec du travail encore ouvert
Le résultat : Claude passe du mode best-effort au mode garantie de finir.
Prochaines étapes
- Lis le guide Hooks principal pour voir tous les types de hooks
- Configure le Context Recovery pour que les sessions survivent à la compaction
- Configure le Skill Activation pour le chargement automatique des skills
- Consulte les Permission Hooks pour les flux d'auto-approbation
Arrêtez de configurer. Commencez à construire.
Templates SaaS avec orchestration IA.