Use PostToolUse Hooks nas ferramentas Write/Edit para rodar testes após cada alteração de arquivo — as falhas são retornadas ao Claude para correção imediata.
Claude Code edita código rápido, mas não executa testes por conta própria. Por padrão, assim que escreve um arquivo considera o trabalho concluído — a menos que você peça explicitamente, ele não sabe se a mudança quebrou alguma coisa.
Um Hook PostToolUse resolve isso: toda vez que Claude escreve um arquivo, os testes rodam automaticamente. Se falharem, o resultado é devolvido diretamente ao Claude para correção imediata.
Claude Code modifica arquivos usando duas ferramentas: Write (criar ou sobrescrever) e Edit (alterações parciais). Adicionar um Hook na fase PostToolUse dessas ferramentas dispara uma execução de testes sempre que um arquivo é salvo em disco.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/run-tests.sh"
}
]
}
]
}
}
O Hook recebe o JSON completo da chamada de ferramenta via stdin, incluindo o caminho do arquivo modificado. Usar esse caminho para decidir quais testes executar é a chave dessa abordagem.
Regras de código de saída (PostToolUse):
- exit 0: testes passaram — Claude continua sem interrupção
- exit 2: testes falharam — o conteúdo do stderr é injetado de volta ao Claude, que vê o erro e tenta corrigir
Abordagem mais simples: verificar a extensão do arquivo e rodar o conjunto de testes correspondente.
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
[[ -z "$file" ]] && exit 0
# Não disparar testes quando Claude edita o próprio arquivo de teste
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 "Testes falharam (arquivo modificado: $file):" >&2
echo "$result" >&2
exit 2
fi
Rodar a suite completa a cada mudança é lento. Use o caminho do arquivo modificado para localizar apenas os testes relevantes.
Em um projeto Python, src/auth/login.py costuma mapear para 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 "Testes falharam:" >&2
echo "$result" >&2
exit 2
fi
Para JavaScript/TypeScript, o Jest suporta filtragem por padrão de nome de arquivo:
#!/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 "Testes falharam:" >&2
echo "$result" >&2
exit 2
fi
Cobrir tanto Write quanto Edit, com proteção de timeout para evitar que testes travados bloqueiem o 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
# Pular arquivos de teste e arquivos que não são código
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 "⚠ Testes falharam (arquivo: $(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"
}
]
}
]
}
}
Não disparar testes ao editar arquivos de teste
Quando Claude edita um arquivo de teste, o próprio teste ainda não está completo. Executá-lo nesse momento é prematuro. Essa linha de filtro é essencial:
echo "$file" | grep -qE '(test_|_test\.|\.test\.|\.spec\.)' && exit 0
Write e Edit usam nomes de campo diferentes em tool_input
Write usa file_path, Edit usa path. Tratar os dois com um fallback:
file=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')
Controlar o timeout dos testes
Suites lentas combinadas com edições frequentes podem travar a sessão. timeout 60 é um padrão razoável; ajuste conforme a duração real dos testes do projeto.
Nível de projeto, não global
Este Hook pertence à configuração de nível de projeto (.claude/settings.json). Os comandos de teste variam demais entre projetos. Para uma versão global, o script precisa detectar o tipo de projeto dinamicamente.
Com essa configuração, o ritmo de trabalho do Claude fica assim:
Sem precisar ficar dizendo "roda os testes". Cada mudança do Claude tem uma rede de segurança de testes.