o problema
Um agente que edita arquivos e roda comandos no seu repositório é probabilístico em tudo, inclusive nas coisas que você não queria que fossem probabilísticas. Você escreve no CLAUDE.md que ele deve rodar o formatador depois de cada edição, e ele roda — quase sempre. Escreve que ele nunca deve tocar em arquivo de credencial, e ele obedece — quase sempre. Instrução em linguagem natural compete com o resto do contexto, e “quase sempre” é um número ruim para uma regra que existe justamente porque a exceção dói.
O outro lado do problema é menos dramático e igualmente chato: o agente é um processo opaco no meio do seu ambiente. Ele muda de diretório, abre sessão, compacta contexto, chama ferramenta de servidor externo. Nada disso emite evento que você possa gravar, medir ou reagir. Quem quer plugar o agente em CI, em notificação de desktop, em auditoria de conformidade ou em direnv não tinha onde encaixar o fio.
a ideia
Hooks tiram a decisão do modelo. Um hook é um comando de shell, um endpoint HTTP, uma chamada de ferramenta MCP, um prompt ou um subagente que dispara automaticamente em um ponto nomeado do ciclo de vida do Claude Code. O ponto é fixo e o código roda sempre, independentemente do que o modelo pretendia. Onde antes você pedia, agora você intercepta.
Os eventos cobrem três cadências: uma vez por sessão (SessionStart, SessionEnd), uma vez por turno (UserPromptSubmit, Stop, StopFailure) e a cada chamada de ferramenta dentro do loop agêntico (PreToolUse, PostToolUse). Em volta disso há dezenas de eventos mais específicos — compactação, troca de modelo, criação de worktree, carregamento de CLAUDE.md, elicitação de MCP. Os mesmos eventos disparam no terminal, na extensão de IDE, no app de desktop e na web.
como funciona
A configuração tem três níveis: escolha o evento, adicione um matcher group que filtra quando ele dispara, defina os handlers que rodam. O matcher casa contra um campo diferente por evento — tool_name nos eventos de ferramenta, motivo de início em SessionStart, tipo de agente em SubagentStart. Se o matcher só contém letras, dígitos, _, -, espaço, , e |, ele é comparado como string exata ou lista de strings exatas; qualquer outro caractere joga a string para o caminho de expressão regular não ancorada.
O handler recebe o JSON do evento no stdin (ou no corpo do POST, se for HTTP) e devolve a decisão pelo exit code e pelo stdout:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked by hook"
}
}
Exit 0 sem saída significa “não tenho decisão” e a chamada segue pelo fluxo normal de permissão — silêncio não aprova nada. Exit 2 bloqueia, e bloqueia mesmo que o JSON diga allow. O JSON dá o controle fino: permissionDecision com allow, deny, ask ou defer; updatedInput para reescrever os argumentos da ferramenta antes de ela rodar; updatedToolOutput para reescrever o resultado depois; additionalContext para injetar texto na janela; continue: false para parar Claude de vez.
Handlers que casam rodam em paralelo. Definir o mesmo handler em dois arquivos de settings roda uma vez só. E há o campo if, com sintaxe de regra de permissão (Bash(git *), Edit(*.ts)), que filtra antes do spawn — existe para você não pagar um processo por chamada de ferramenta.
o que isso custou
A própria documentação diz onde a coisa não chega: “use o sistema de permissão em vez de um hook para impor allow ou deny duro”. O filtro if é best-effort — quando o Claude Code não consegue determinar quais comandos um input de Bash executa, ele roda o hook de qualquer jeito, pelo sim pelo não. Um hook de PreToolUse que estoura o timeout tem a saída descartada e a chamada continua. Exit 1 não bloqueia. Todo o desenho falha aberto, o que é a escolha certa para automação e a errada para uma fronteira de confiança.
O custo operacional é real: cada evento de ferramenta pode virar um processo novo, e SessionStart roda em toda sessão. Saída de hook é capada em 10.000 caracteres; acima disso vira arquivo e preview. E hook é código arbitrário rodando com o seu ambiente, o que abriu uma superfície que precisou de resposta administrativa própria — workspace trust para frontmatter de projeto, allowManagedHooksOnly para empresa, allowlist de URL para hooks HTTP, e um disableAllHooks que só desliga hook gerenciado se ele mesmo vier do nível gerenciado.
onde isso aparece hoje
Hooks deixaram de ser configuração de settings.json e viraram peça de composição. Agent Skills declaram hooks no frontmatter, que ficam registrados pelo resto da sessão a menos que você marque once: true. Claude Code subagents declaram os seus, e o Claude Code converte um Stop de subagente em SubagentStop. Plugins empacotam um hooks/hooks.json.
O Model Context Protocol entra pelos dois lados: ferramentas MCP aparecem como ferramentas comuns nos eventos e podem ser filtradas por matcher, e um hook pode ser ele mesmo uma chamada de ferramenta MCP. O Claude Agent SDK expõe hooks como callbacks, com uma diferença de semântica que vale saber: lá, um callback de PreToolUse que estoura o timeout bloqueia a chamada, em vez de deixar passar.