Hooks do Claude Code: como transformar regras do agente em garantias
Hooks do Claude Code são comandos de shell que o próprio Claude Code executa em eventos específicos: antes e depois de uma chamada de ferramenta, ao enviar um prompt, ao iniciar a sessão e antes de o agente parar. Eles são configurados no settings.json, e o código de saída 2 bloqueia a ação e devolve ao agente uma mensagem de erro. Assim, o hook transforma uma regra do CLAUDE.md em uma garantia que o modelo não consegue esquecer.
O que são hooks do Claude Code
Um hook é um comando executado não pelo modelo, mas pelo próprio Claude Code em um momento definido do trabalho do agente. O agente está prestes a rodar rm -rf — o hook é acionado e decide se deixa o comando passar. O agente editou um arquivo — o hook roda o formatador. O agente quer encerrar o trabalho — o hook roda os testes e impede a parada enquanto eles estiverem vermelhos.
A diferença principal em relação às instruções do CLAUDE.md: uma instrução é texto que o modelo leva em conta com alguma probabilidade, enquanto um hook é código determinístico. Ele sempre é executado, qualquer que seja o tamanho do contexto, o que o agente tenha «decidido» ou se ele esqueceu a regra na centésima mensagem da sessão. Por isso, hooks do Claude Code são o principal meio de transformar um desejo em garantia.
Por que as regras no CLAUDE.md não bastam
Uma cena típica: o agente terminou a funcionalidade, os testes unitários estão verdes, e no chat aparece um «Pronto!» confiante. Só que ninguém rodou o cenário de ponta a ponta, o linter não foi executado, e a migração falhou no meio do caminho. No CLAUDE.md está escrito «antes de terminar, rode os testes». O modelo não está mentindo de propósito — ele apenas avalia a si mesmo, e essa autoavaliação não é confiável.
Daí o princípio do harness: tudo o que pode ser verificado mecanicamente é verificado pela mecânica, e não pelo modelo. A regra «rode o linter» vira um hook que roda o linter. A regra «não toque na configuração de produção» vira um hook que bloqueia a gravação nesse arquivo. A instrução textual permanece, mas serve apenas para explicar ao agente por que ele foi interrompido.
Eventos: quando os hooks disparam
Cada hook está ligado a um evento do ciclo de vida da sessão. Os principais eventos:
| Evento | Quando dispara | Uso típico |
|---|---|---|
PreToolUse | antes da chamada de uma ferramenta (Bash, Edit, Write etc.) | bloqueio de comandos perigosos, proteção de arquivos |
PostToolUse | depois de uma chamada de ferramenta bem-sucedida | formatador, linter, testes rápidos após a edição |
UserPromptSubmit | quando o usuário envia um prompt, antes do processamento | adicionar contexto, barrar um prompt com segredo |
Stop | quando o agente está prestes a concluir a resposta | impedir a parada enquanto as verificações não passarem |
SubagentStop | quando um subagente termina o trabalho | verificação do resultado do subagente |
SessionStart | ao iniciar ou retomar a sessão | carregar o estado: git status, plano, diário de progresso |
SessionEnd | ao encerrar a sessão | limpeza, gravação de logs |
Notification | quando o Claude Code envia uma notificação (por exemplo, aguardando permissão) | som ou mensagem no celular |
PreCompact | antes da compactação do contexto | salvar o que é importante antes da compactação |
Os mais úteis para a confiabilidade são PreToolUse, PostToolUse e Stop: eles cobrem os três pontos fracos do agente — ações perigosas, edições não verificadas e o «pronto» dado cedo demais.
Onde os hooks são configurados: settings.json
Os hooks são descritos nos arquivos de configuração do Claude Code:
~/.claude/settings.json— pessoal, vale para todos os projetos;.claude/settings.jsonno repositório — compartilhado pela equipe, versionado no git;.claude/settings.local.json— local do projeto, fora do git.
A estrutura é: evento → lista de grupos com matcher → lista de comandos.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint.sh",
"timeout": 60
}
]
}
]
}
}
matcher é o nome de uma ferramenta ou uma expressão regular sobre ele: Bash, Edit|Write, .*. Ele é necessário para eventos ligados a ferramentas; para Stop ou UserPromptSubmit pode ser omitido. A variável CLAUDE_PROJECT_DIR aponta para a raiz do projeto, por isso é cômodo guardar os scripts em .claude/hooks/ e chamá-los por ela. Para ver os hooks atuais, use o comando /hooks dentro do Claude Code.
Como o hook recebe os dados e responde
Entrada: JSON no stdin
O Claude Code passa ao hook um JSON com os dados do evento: identificador da sessão, diretório de trabalho, nome do evento e, para ferramentas, tool_name e tool_input. No Bash, tool_input.command contém o comando; no Edit e no Write, tool_input.file_path contém o caminho do arquivo. É prático ler isso com jq.
{
"session_id": "abc123",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "rm -rf build/" },
"cwd": "/home/dev/project"
}
Saída: código de retorno
| Código | O que acontece |
|---|---|
0 | sucesso, o trabalho continua; para UserPromptSubmit e SessionStart, o stdout é acrescentado ao contexto do agente |
2 | erro bloqueante: o stderr é repassado ao agente e a ação é interrompida (em PreToolUse a chamada da ferramenta é cancelada; em Stop o agente continua trabalhando) |
| outro | erro não bloqueante: o stderr é mostrado ao usuário, e o trabalho segue |
O exit code 2 é o mecanismo-chave. O agente recebe o texto do stderr como retorno e pode se corrigir: reescrever o comando, consertar o teste, não tocar no arquivo. Por isso, escreva no stderr não «erro», e sim a causa clara e o que fazer no lugar.
Além do código de retorno, o hook pode imprimir no stdout um JSON com a decisão, por exemplo "decision": "block" e "reason". Isso serve para ajustes finos, mas para a maioria dos casos bastam o código 2 e a mensagem no stderr.
Exemplo 1: linter e testes após cada edição
Um hook em PostToolUse com matcher Edit|Write é acionado depois que um arquivo é alterado. A ferramenta já foi executada, então a edição não pode ser desfeita, mas o código 2 devolve ao agente a saída do linter, e ele corrige o erro na hora, não meia hora depois.
#!/usr/bin/env bash
# .claude/hooks/lint.sh
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*.py)
if ! out=$(ruff check "$file" 2>&1); then
echo "O linter encontrou erros em $file:" >&2
echo "$out" >&2
exit 2
fi ;;
*.php)
if ! out=$(php -l "$file" 2>&1); then
echo "$out" >&2
exit 2
fi ;;
esac
exit 0
Não coloque aqui uma execução completa de testes de dez minutos: o hook dispara a cada edição. Após a edição, faça apenas verificações rápidas (sintaxe, linter, testes de um módulo), e as pesadas, no hook Stop.
Exemplo 2: bloqueio de comandos perigosos
Um hook em PreToolUse com matcher Bash vê o comando antes da execução. O código 2 cancela a chamada, e o agente recebe a explicação.
#!/usr/bin/env bash
# .claude/hooks/guard.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'rm -rf /|git push (-f|--force)|DROP (TABLE|DATABASE)'; then
echo "Comando bloqueado pela política do projeto: $cmd. Operações perigosas são executadas por uma pessoa." >&2
exit 2
fi
exit 0
Arquivos também podem ser protegidos assim: um hook em Edit|Write verifica o file_path e bloqueia a gravação em .env, configurações de produção, código gerado ou migrações já aplicadas. Atenção: a busca por expressão regular pode ser contornada — o agente pode escrever o comando de outra forma. O hook é uma proteção forte, mas não substitui as permissões de acesso nem o sandbox.
Exemplo 3: o agente não pode dizer «pronto» enquanto os testes estiverem vermelhos
O hook Stop é acionado quando o agente está prestes a terminar. Se os testes não passam, o código 2 impede a parada, e a saída dos testes vai para o agente como próxima tarefa. É a versão mecânica da Definition of Done.
#!/usr/bin/env bash
# .claude/hooks/done.sh
input=$(cat)
# proteção contra loop infinito: o hook já prolongou o trabalho uma vez
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
if ! out=$(npm test --silent 2>&1); then
echo "Os testes falharam, o trabalho não está concluído:" >&2
echo "$out" | tail -n 40 >&2
exit 2
fi
exit 0
O campo stop_hook_active é true quando o agente já está continuando o trabalho por causa de um hook de parada. Sem essa verificação, o hook pode ficar mandando o agente em círculos sem fim — exatamente a situação em que o agente «trabalha a noite toda na direção errada». Em ciclos autônomos, acrescente também um limite de tentativas e uma condição em que uma pessoa é chamada.
Exemplo 4: carregar o estado no início da sessão
No SessionStart, o stdout do hook vai para o contexto. É útil exibir a branch atual, as alterações não commitadas e a próxima tarefa do plano, para que o agente não comece do zero.
#!/usr/bin/env bash
echo "Branch: $(git branch --show-current)"
git status --short | head -n 20
[ -f PROGRESS.md ] && tail -n 15 PROGRESS.md
exit 0
Como os hooks transformam regras em garantias
É útil dividir as regras do harness em três níveis. O primeiro é o texto no CLAUDE.md: explica como trabalhar, mas é cumprido de forma probabilística. O segundo são as verificações executáveis: testes, E2E, regras de arquitetura em forma de script que falha quando há violação. O terceiro são os hooks, que disparam essas verificações automaticamente no momento certo, sem participação do modelo.
Regra prática: se o agente violou a mesma regra duas vezes, não acrescente mais um alerta em letras maiúsculas no CLAUDE.md. Pergunte se a violação pode ser verificada por um script. Se puder, crie o script e prenda-o a um hook. Assim a instrução textual fica mais curta e o comportamento, mais estável.
| Regra no texto | Garantia por meio de hook |
|---|---|
| «Formate o código conforme o padrão» | PostToolUse roda o formatador no arquivo alterado |
| «Não faça push com force» | PreToolUse no Bash bloqueia o comando, exit 2 |
| «Não diga pronto enquanto os testes estiverem vermelhos» | Stop roda os testes e impede a conclusão |
| «Não edite o .env» | PreToolUse em Edit|Write verifica o caminho |
| «Lembre onde parou» | SessionStart exibe o plano e o git status |
Erros típicos
- Código 1 em vez de 2. O script falha com código 1, o agente não fica sabendo, e a ação passa. Só o código 2 bloqueia.
- Mensagem no stdout. Com código 2, o agente recebe o stderr. Escreva com
>&2. - Hook lento em um evento frequente. A execução completa dos testes em PostToolUse torna cada edição um processo de minutos. As tarefas pesadas vão para o Stop.
- Stop infinito. Sem a verificação de
stop_hook_activeou sem limite, o agente gira sem parar. - Caminhos relativos. O hook roda no diretório de trabalho da sessão; use
$CLAUDE_PROJECT_DIR. - Hook de terceiros não verificado. O hook é executado com as suas permissões, sem confirmação. Leia os scripts de repositórios alheios antes de confiar neles.
Resumo
Os hooks do Claude Code levam as regras mais importantes do texto para o código: o PreToolUse interrompe ações perigosas, o PostToolUse verifica cada edição, o Stop impede a declaração de conclusão antes da hora, e o SessionStart devolve ao agente a memória do projeto. Comece com dois ou três hooks para as regras que o agente já violou, e mantenha as mensagens no stderr claras — assim o agente corrige os erros sozinho, e você verifica o resultado, e não o processo. Validação, verificação de conclusão e ciclos autônomos são tratados na prática no curso.
Perguntas frequentes
Preciso reiniciar o Claude Code depois de alterar os hooks?
Mudanças no settings.json são aplicadas com mais segurança em uma nova sessão; a configuração atual dos hooks pode ser vista com o comando /hooks.
Dá para escrever hooks que não sejam em bash?
Sim, um hook é qualquer comando: um script em Python, Node.js ou um binário. O importante é ler o JSON do stdin e devolver o código de saída correto.
O agente vê a saída de um hook que terminou com sucesso?
Normalmente não: com código 0, a saída não entra no contexto. As exceções são UserPromptSubmit e SessionStart, em que o stdout é acrescentado ao contexto do agente.
Os hooks funcionam para subagentes?
Sim, as chamadas de ferramentas dos subagentes passam pelos mesmos hooks PreToolUse e PostToolUse, e para o encerramento deles existe o evento separado SubagentStop.
É seguro versionar os hooks no repositório?
Sim, o .claude/settings.json e os scripts em .claude/hooks/ são prática comum em equipe; hooks pessoais ficam no settings.local.json ou no ~/.claude/settings.json.
Este tema é abordado em profundidade, com prática, no curso “Harness Engineering: инженерия надёжности AI-агентов”.
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента
- Автономный AI-агент часами без присмотра: Loop Engineering и условия остановки