Stop Hooks
Stop hooks impedem o Claude Code de encerrar uma resposta enquanto os testes falham, o build quebra ou o lint está vermelho. Quatro padrões de imposição mais proteções contra loops infinitos.
Pare de configurar. Comece a construir.
Templates SaaS com orquestração de IA.
Problema: O Claude termina uma resposta, mas o trabalho não está de fato concluído. Os testes ainda falham. Arquivos estão pela metade. Você pergunta "terminou?" e recebe um sim, enquanto o build está vermelho.
Solução rápida: Adicione este Stop hook e o Claude não pode encerrar o turno enquanto os testes não estiverem verdes:
{
"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)A partir daqui, o Claude literalmente não consegue fechar um turno com uma suite com falhas.
Como o Stop Hook Funciona
Cada vez que o Claude vai terminar uma resposta, este hook dispara. Três coisas podem acontecer:
- Permitir parada - Sair com código 0 e o turno termina normalmente.
- Bloquear parada - Retornar
{"decision": "block", "reason": "..."}e o Claude continua. - Executar validações - Disparar testes, verificações ou qualquer script que você quiser.
O Payload
{
"session_id": "uuid-string",
"stop_hook_active": false,
"transcript_path": "/path/to/transcript.jsonl"
}Preste atenção em stop_hook_active. Um valor verdadeiro significa que o Claude já está em estado de continuação forçada por um bloqueio anterior. Ignorar esse flag gera um loop sem fim.
Padrão 1: Gate de Testes
Mantém o turno aberto até todos os testes passarem:
#!/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)Padrão 2: Validação de Build
Bloqueia até o projeto compilar:
#!/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)Padrão 3: Verificação de Lint
Sem sair do turno com erros de lint na fila:
#!/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)Padrão 4: Marcador de Conclusão de Tarefa
Controla o turno com base em um flag de tarefa específico:
#!/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)Crie o marcador ao iniciar o trabalho:
echo "Implement user authentication" > .claude/incomplete-task
Remova quando o trabalho estiver concluído:
rm .claude/incomplete-task
Prevenindo Loops Infinitos
É por isso que o flag stop_hook_active importa. Sem ele você tem isto:
Claude responds → Stop hook fires → "block" → Claude continues
↓
Claude responds → Stop hook fires → INFINITE LOOP (without flag check)Sempre verifique o flag primeiro:
if input_data.get('stop_hook_active', False):
sys.exit(0) # Allow stopping, break the loopCombinando Múltiplas Verificações
Um hook pode encadear vários gates:
#!/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)Quando Usar Stop Hooks
Bons casos de uso:
- Controlar um suite de testes verde antes de "tarefa concluída"
- Garantir que o build ainda compila
- Capturar erros de lint e de tipo
- Qualquer regra personalizada do que "pronto" significa para você
Maus casos de uso:
- Qualquer coisa que rode mais do que o timeout de 60 segundos
- Verificações que acessam a rede e falham intermitentemente
- Prompts que precisam de uma resposta humana (sem interação aqui)
Configuração
Configure em .claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python .claude/hooks/stop-validation.py"
}
]
}
]
}
}Vários hooks podem rodar ao mesmo tempo, em paralelo. Um block de qualquer um deles mantém o Claude em andamento.
Depuração
Preso em um loop?
- Verifique duas vezes se você lê
stop_hook_activeno início do script - Registre:
echo "stop_hook_active: $stop_hook_active" >> ~/.claude/stop-debug.log
Bloqueio não está funcionando?
- O JSON deve ser
{"decision": "block", "reason": "..."} - Use código de saída 0, não 2. Código de saída 2 é para um caminho de bloqueio diferente.
Testes demorando muito?
- O timeout do hook é 60 segundos
- Execute um subconjunto menor, ou acelere a suite
O Padrão "Ralph Wilgum"
Este vem de uma técnica da comunidade e usa Stop hooks para forçar um loop de tarefa persistente:
- Crie um marcador de tarefa no início da sessão
- Faça o Stop hook bloquear enquanto o marcador está presente
- Exija que o Claude delete o marcador como prova de conclusão
- Sem mais "estou pronto" acidental com trabalho ainda aberto
O resultado: o Claude passa de melhor esforço para conclusão garantida.
Próximos Passos
- Leia o Guia de Hooks principal para ver todos os tipos de hook
- Configure o Context Recovery para que as sessões sobrevivam à compactação
- Configure o Skill Activation para carregamento automático de skills
- Veja os Permission Hooks para fluxos de aprovação automática
Pare de configurar. Comece a construir.
Templates SaaS com orquestração de IA.