Hooks no Claude Code: guia prático

Downshift por Tiago Vilas Boas. Atualizado em

Este Downshift é uma ferramenta em Go para agentes de código com IA, não a biblioteca React downshift de autocomplete.

Resumo

Hooks no Claude Code executam código seu em pontos fixos da sessão. Um hook PreToolUse vê cada chamada de ferramenta antes de ela rodar e pode permitir, negar, perguntar ou reescrever a entrada com updatedInput. Por padrão, falha não bloqueia. Este guia cobre os 33 eventos, configuração, códigos de saída, matchers e três exemplos seguros, e mostra como o Downshift usa dois hooks PreToolUse em produção.

O que são hooks no Claude Code?

Hooks são código seu que o Claude Code executa em pontos fixos da sessão: antes de uma ferramenta rodar, depois dela, quando você envia um prompt, quando um subagente termina, entre outros. Hook não é dica para o modelo. É um handler que roda toda vez que o evento acontece, e por isso é o lugar das regras que você não quer que o modelo esqueça.

Na maioria dos casos o handler é um comando de shell. A referência também traz os tipos http, mcp_tool, prompt e agent. Um hook de comando lê um JSON no stdin e responde com um código de saída e, se quiser, um objeto JSON no stdout.

Quais eventos de hook o Claude Code tem?

A referência lista 33 eventos. Abaixo está a lista publicada, um por linha. PreToolUse, UserPromptExpansion e PreModelSwitch são os que a tabela marca como capazes de bloquear.

EventoDispara quando
SessionStartUma sessão começa ou é retomada
SetupO Claude Code inicia com --init-only, ou com --init ou --maintenance no modo -p
UserPromptSubmitUm prompt é enviado, antes de o Claude processá-lo
UserPromptExpansionUm comando digitado vira prompt; pode bloquear a expansão
PreToolUseAntes de uma chamada de ferramenta rodar; pode bloqueá-la
PermissionRequestUma chamada de ferramenta precisa de decisão de permissão
PermissionDeniedO modo auto nega uma chamada de ferramenta
PostToolUseDepois que uma chamada de ferramenta dá certo
PostToolUseFailureDepois que uma chamada de ferramenta falha
PostToolBatchDepois que um lote de chamadas paralelas termina, antes da próxima chamada ao modelo
NotificationO Claude Code envia uma notificação
MessageDisplayO texto de uma resposta do assistente é exibido
SubagentStartUm subagente é criado
SubagentStopUm subagente termina
TaskCreatedUma tarefa está sendo criada via TaskCreate
TaskCompletedUma tarefa está sendo marcada como concluída
StopO Claude termina de responder
StopFailureO turno termina por erro de API
TeammateIdleUm colega de um time de agentes vai ficar ocioso
InstructionsLoadedUm CLAUDE.md ou .claude/rules/*.md é carregado
ConfigChangeUm arquivo de configuração muda durante a sessão
CwdChangedO diretório de trabalho muda
DirectoryAddedUm diretório de trabalho é adicionado no meio da sessão
FileChangedUm arquivo observado muda no disco
WorktreeCreateUm worktree está sendo criado
WorktreeRemoveUm worktree criado por um hook WorktreeCreate está sendo removido
PreCompactAntes da compactação do contexto
PostCompactDepois que a compactação do contexto termina
PreModelSwitchAntes de aplicar uma troca de modelo pedida; pode bloqueá-la
PostModelSwitchDepois que o modelo da sessão muda
ElicitationUm servidor MCP pede uma entrada do usuário durante uma chamada
ElicitationResultDepois que o usuário responde a esse pedido do MCP
SessionEndUma sessão termina

Verificado em 2026-10-24 na referência de hooks do Claude Code. Eventos, padrões e campos mudam entre versões; confira a sua.

Onde configurar hooks no Claude Code?

Os hooks ficam sob a chave hooks, e as entradas de cada nível se somam em vez de se substituírem.

  • ~/.claude/settings.json: todos os projetos da sua máquina.
  • .claude/settings.json: um projeto, compartilhado pelo repositório.
  • .claude/settings.local.json: um projeto, fora do git.
  • Settings gerenciadas: valem para a organização, definidas por um admin.
  • hooks/hooks.json de um plugin: ativo enquanto o plugin estiver habilitado.
  • Frontmatter de skill e de subagente: ativo enquanto aquela skill ou subagente estiver em uso.

A estrutura tem três níveis: evento, grupo com matcher e handlers.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/check-bash.sh", "timeout": 10 }
        ]
      }
    ]
  }
}

A página de settings do Claude Code explica a precedência das outras chaves, e o guia de hooks faz um passo a passo.

Como o hook conversa com o Claude Code: entrada, saída e códigos de saída

Todo hook de comando recebe no stdin um JSON com campos comuns como session_id, transcript_path, cwd, permission_mode e hook_event_name. Eventos de ferramenta somam tool_name, tool_input e tool_use_id. Dentro de um subagente chegam também agent_id e agent_type.

  • Saída 0: sucesso. Se o stdout for um único objeto JSON, o Claude Code lê como decisão; se não, vira texto.
  • Saída 2: erro que bloqueia, nos eventos que podem bloquear. O stderr vira o motivo. No PostToolUse ele só mostra o stderr, porque a ferramenta já rodou.
  • Qualquer outro código: erro que não bloqueia. A ação segue e aparece um aviso de erro do hook. Saída 1 não bloqueia; para política, use saída 2 ou um deny em JSON.

Deixe o stdout limpo. Um echo esquecido antes do JSON transforma a decisão em texto.

Como um hook PreToolUse permite, nega, pergunta ou reescreve uma chamada?

Este é o evento que mais importa. O hook PreToolUse responde com hookSpecificOutput contendo hookEventName: "PreToolUse", um permissionDecision e um permissionDecisionReason.

permissionDecisionO que faz
allowPula o pedido de permissão. Regras deny e ask das suas settings continuam valendo.
denyImpede a chamada; o motivo vai para o Claude.
askPede sua confirmação, com o motivo no prompt, até no modo auto.
deferEncerra o turno para retomar a ferramenta depois; só com -p, ignorado no modo interativo.

Quando vários hooks discordam, a referência define a precedência deny > defer > ask > allow.

O quarto poder é reescrever. O updatedInput, dentro de hookSpecificOutput, troca o objeto de entrada inteiro da ferramenta antes de ela rodar. Combine com allow para aprovar direto, ou com ask para mostrar a entrada alterada ao usuário. Duas regras pegam muita gente: devolva todos os campos que não mudaram, e lembre que as regras de permissão são avaliadas sobre a entrada que o hook devolve, não sobre a que o Claude mandou.

Verificado em 2026-10-24 na referência de hooks do Claude Code. Eventos, padrões e campos mudam entre versões; confira a sua.

Como funcionam os matchers dos hooks?

  • "*", "" ou sem matcher: casa com tudo.
  • Só letras, dígitos, _, -, espaço, vírgula e barra vertical: nome exato, ou lista como Edit|Write.
  • Qualquer outro caractere: expressão regular JavaScript sem âncora. Edit.* também pega NotebookEdit; use ^Edit$ para o nome inteiro. mcp__memory__.* pega todas as ferramentas de um servidor MCP.

Em eventos de ferramenta, o handler aceita um campo if na sintaxe das regras de permissão, como Bash(git *), para rodar só em algumas chamadas. A referência chama esse filtro de best-effort: ele reduz quando o hook roda, não é sua fronteira de segurança.

O que acontece quando um hook estoura o timeout ou falha?

Os padrões da referência: 600 segundos para handlers command, http e mcp_tool, 30 segundos para prompt, 60 para agent, e 30 segundos no UserPromptSubmit e nos eventos de troca de modelo. Defina seu próprio timeout, em segundos, em cada handler.

No PreToolUse, um hook que quebra ou estoura o tempo não bloqueia a chamada. Ela segue pelo fluxo normal de permissão, como se o hook não tivesse dito nada. Se você quer que a falha bloqueie, use "onFailure": "block"; a referência indica que existe a partir da v2.1.295. Já no PreModelSwitch, o timeout bloqueia a troca.

Esse padrão acerta na maioria dos casos: um bug no seu logger não deve parar a sessão. É também por isso que hook é guarda-corpo, não cadeado.

Exemplos de hooks no Claude Code: três hooks seguros e mínimos

Cada script precisa de jq e de chmod +x. Cada um faz uma coisa só e fica quieto no resto.

1. Bloquear um comando destrutivo

Nega rm -rf e deixa passar o resto do Bash. O filtro if evita que o script rode em comandos que não interessam.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "if": "Bash(rm *)", "command": "~/.claude/hooks/block-rm-rf.sh" }
        ]
      }
    ]
  }
}
#!/bin/bash
# block-rm-rf.sh: nega rm -rf, fica em silêncio no resto
cmd=$(jq -r '.tool_input.command // ""')
if printf '%s' "$cmd" | grep -Eq 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f|rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r'; then
  jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason: "rm -rf bloqueado por um hook local"}}'
fi
exit 0

Para uma regra dura, acrescente também um deny em permissions nas settings; a referência recomenda o sistema de permissões para isso.

2. Registrar chamadas sem o conteúdo

Um rastro de quais ferramentas rodaram e quando, sem argumentos, caminhos de arquivo ou saída. O async tira o log do caminho crítico.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/log-tool.sh", "async": true }
        ]
      }
    ]
  }
}
#!/bin/bash
# log-tool.sh: só nome da ferramenta, sessão e hora; nunca tool_input nem saída
jq -c '{ts: (now | todate), session: .session_id, tool: .tool_name}' \
  >> ~/.claude/tool-calls.jsonl

3. Trocar o modelo de um subagente

Devolva o tool_input original mudando só o model. Use o nome da família; em 2026-10-06 um id completo de modelo foi recusado e o subagente nem subiu.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Agent|Task",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/subagent-model.sh" }
        ]
      }
    ]
  }
}
#!/bin/bash
# subagent-model.sh: devolve o tool_input inteiro, muda só o model
jq '{hookSpecificOutput: {hookEventName: "PreToolUse",
  permissionDecision: "allow",
  updatedInput: (.tool_input + {model: "haiku"})}}'

Este responde sempre haiku: mostra o mecanismo, não é uma política. O guia de PreToolUse para o modelo do subagente (em inglês) traz os detalhes.

Hooks no Claude Code são seguros? Cuidados de segurança

  • Hooks de comando rodam com as permissões do seu usuário, sem sandbox próprio. O que seu shell faz, o hook faz.
  • Revise hook ou plugin de terceiros como código que você vai executar, porque é isso. Leia o script, o matcher e o que ele grava em disco ou manda pela rede.
  • Hooks de projeto de um repositório clonado só rodam depois que você aceita o diálogo de confiança do workspace. Não aceite em repositório que você não leu.
  • Admins podem ligar allowManagedHooksOnly para bloquear hooks de usuário, projeto e plugin; hooks HTTP podem ser limitados com allowedHttpHookUrls.
  • Prefira a forma exec (args) para evitar que o shell interprete caracteres especiais.
  • Silêncio de hook nunca aprova chamada. Deixe allow e deny duros nas permissões; use hook para a lógica que regra não expressa.

Exemplo real: como o Downshift usa hooks PreToolUse

O Downshift é um binário Go open source que traz dois hooks PreToolUse para o Claude Code, os dois registrados pelo plugin.

O hook de subagente: matcher Agent|Task

Quando o Claude cria um subagente, o Downshift pontua o texto da tarefa localmente, sem chamar LLM, e escolhe um nível. Se está confiante, devolve updatedInput com os mesmos campos e só model e reasoning_effort alterados. Se está em dúvida, segura, e o subagente roda exatamente como o Claude pediu.

// tool_input que o Claude enviou
{ "description": "Rename variable", "prompt": "rename userId to accountId",
  "subagent_type": "general-purpose", "model": "opus" }

// updatedInput que o Downshift devolve: mesmos campos, só o model mudou
{ "description": "Rename variable", "prompt": "rename userId to accountId",
  "subagent_type": "general-purpose", "model": "haiku" }
  • Falha aberta. O stdin tem teto de 1 MiB e leitura com timeout de 2 segundos, o hook inteiro tem prazo de 1,5 segundo, e um panic deixa o subagente como estava.
  • O texto da tarefa fica só na memória. É pontuado e descartado, nunca vai para o disco; a telemetria guarda contagens, hashes e tempos.
  • Os outros campos não mudam. prompt, description e subagent_type voltam como chegaram.

O Compactor: matcher Bash

Quando o agente roda um comando de teste, build ou status que está na allowlist (go test, git status, pytest, vitest, jest, tsc, cargo test e outros), o hook reescreve o command para passar pelo downshift compact. Execução verde volta como uma linha de resumo, falha volta inteira, e o código de saída real é mantido.

# o que o agente pediu
go test ./...
# o que roda depois do hook PreToolUse de Bash (allowlist, sem metacaracteres)
downshift compact -- go test ./...

Ele só reescreve comandos sem metacaracteres de shell, e uma regra deny ou ask das suas settings que case com o comando impede a reescrita: o hook nunca passa um comando por cima das suas regras. "exec": false ou DOWNSHIFT_EXEC=0 desliga.

Perguntas frequentes

O que é um hook no Claude Code?

Um handler, quase sempre um comando de shell, que o Claude Code executa num ponto fixo da sessão, como antes de uma chamada de ferramenta. O hook de comando lê um JSON no stdin e responde com um código de saída e, se quiser, um objeto JSON no stdout.

Um hook PreToolUse consegue mudar a entrada de uma ferramenta?

Consegue. Devolva updatedInput dentro de hookSpecificOutput junto com permissionDecision allow ou ask. Ele substitui o objeto de entrada inteiro, então devolva também os campos que não mudaram.

Um hook com bug trava o Claude Code?

No PreToolUse, por padrão, não. Saída 2 ou uma decisão deny bloqueiam. Crash, timeout ou qualquer outro código deixam a chamada seguir pelo fluxo normal de permissão, a menos que o hook use onFailure block.

O allow de um hook passa por cima das minhas regras deny?

Não. Segundo a referência de hooks, as regras deny e ask das suas settings são avaliadas de qualquer jeito, e sobre a entrada que o hook devolve.

O Downshift lê ou guarda meus prompts?

Ele lê o texto da tarefa do subagente para pontuar, só na memória, e nunca grava. Muda apenas model e reasoning_effort e, em qualquer erro, deixa o subagente como estava.

Coloque um hook para trabalhar em cada subagente

Instale o Downshift, veja cada alteração antes com downshift setup --dry-run, e deixe os dois hooks PreToolUse rotearem subagentes e compactarem a saída de teste. downshift setup --undo restaura os backups.

curl -fsSL https://raw.githubusercontent.com/tiagovilasboas/downshift/main/install.sh | sh && downshift setup

Plugin do Claude Code

/plugin install downshift --marketplace tiagovilasboas/downshift

Guias relacionados

Fontes

  1. Anthropic docsverificado
    code.claude.com/docs/en/hooks
    lista de eventos, campos de entrada, códigos de saída, decisões do PreToolUse, updatedInput, matchers, timeouts, segurança (consultado em 2026-10-24)
  2. Anthropic docsverificado
    code.claude.com/docs/en/hooks-guide
    passo a passo de configuração (link, sem citação)
  3. Anthropic docsverificado
    code.claude.com/docs/en/settings
    arquivos e escopos de settings (link, sem citação)
  4. Downshift repoverificado
    docs/evidence/four-arm-mechanical-2026-10-21.md
    números da bateria de quatro braços, 2026-10-21
  5. Downshift repoverificado
    hooks/hooks.json
    matchers que o Downshift registra no Claude Code