INEMA.CLUBPROHooks do Claude Code

Hooks do Claude Code / Trilha 01

Trilha 01 · Fundamentos

Fundamentos dos hooks

O que é um hook, os eventos que disparam cada um, onde tudo mora no settings.json, o JSON que chega — e como seu hook decide o que responder.

5aulas
~45minutos
iniciantenível

Trilha 01 · Aula 1 · Iniciante

Hooks:
o essencial

Um hook é uma ordem automática: "toda vez que acontecer isto, rode aquilo". É como o Claude Code segue suas regras sozinho — sem depender de lembrar.

role para estudar

01 O que é um hook

Um hook é uma ordem automática que você dá ao Claude Code: "toda vez que acontecer isto, rode aquilo". O isto é um momento (um evento); o aquilo é um comando seu — um script, um formatador, um aviso. O Claude dispara sozinho, sem você pedir de novo.

Sem hook, você repete as mesmas instruções ("formata o código", "não roda isso") a cada sessão, e torce pra elas serem seguidas. Com hook, a regra vira parte do ambiente: automática, e o mesmo comportamento toda vez.

Teste-se

Um hook, na prática, é:

02 Os momentos (eventos)

Cada hook se pendura num evento — um momento do trabalho do Claude. Os que mais importam pra começar:

  • PreToolUseantes do Claude usar uma ferramenta (ex.: antes de rodar um comando no terminal).
  • PostToolUsedepois (ex.: formatar o arquivo assim que ele é editado).
  • UserPromptSubmit — quando você envia uma mensagem.
  • Stop — quando o Claude termina de responder.
  • SessionStart — quando a sessão começa (ótimo pra injetar contexto do projeto).

Cada evento entrega ao seu comando os detalhes do que está acontecendo (qual ferramenta, qual arquivo) em JSON pela entrada padrão. Seu comando lê isso e decide o que fazer.

03 Onde eles moram: settings.json

Hooks vivem no settings.json — no projeto (.claude/settings.json) ou global (~/.claude/settings.json). Você diz o evento, um matcher (em qual ferramenta pega) e o comando:

// .claude/settings.json — formata todo arquivo editado
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "prettier --write $file" }
        ]
      }
    ]
  }
}

Leia de dentro pra fora: no evento PostToolUse, quando a ferramenta for Edit ou Write, rode o comando. Só isso — três camadas: evento → matcher → comando.

Teste-se

O matcher "Edit|Write" serve pra:

04 Um hook que protege

No PreToolUse, seu comando pode bloquear a ação antes dela rodar. Se o comando sai com código 2, o Claude não executa aquilo — e a mensagem que você imprime vira o motivo mostrado a ele. É assim que você barra o perigoso (um rm -rf, um push na main) antes de acontecer.

# bloqueia rm -rf; recebe o comando por stdin (JSON)
grep -q "rm -rf" && { echo "bloqueado: comando destrutivo" >&2; exit 2; }
exit 0

Exit 0 = pode seguir. Exit 2 = barra e devolve o motivo. Simples e poderoso: você transforma "por favor não faça" numa trava real.

Teste-se

Para o hook PreToolUse barrar um comando, ele deve sair com código:

05 Ideias pra começar

Você não precisa de muitos. Comece com um hook que te tire uma preocupação repetida da cabeça:

  • Formatar ao salvar — PostToolUse em Edit/Write roda o formatador. Nunca mais código torto.
  • Avisar quando terminar — Stop dispara uma notificação ou um som.
  • Registrar comandos — PreToolUse em Bash grava num log tudo que foi rodado.
  • Injetar as regras do projeto — SessionStart imprime seu CLAUDE.md, e o Claude já começa alinhado.

O melhor hook é o mais chato de fazer na mão. Automatize esse primeiro.

Indo mais fundo

Além do exit code, hooks podem devolver JSON pela saída (uma decisão de aprovar/bloquear com motivo), usar matchers mais finos, e existem outros eventos: SubagentStop, Notification, PreCompact, SessionEnd. Continua nas próximas aulas.

Pratique agora 0/4 feito

Seu primeiro hook: registrar comandos

Faça o Claude gravar cada comando num arquivo — e veja funcionar. Uns 5 minutos, e seguro: o hook só observa (sai com exit 0), não bloqueia nada.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "cat >> .claude/cmd.log" }
        ]
      }
    ]
  }
}

✓ Pronto: seu primeiro hook rodou sozinho. É exatamente isto — quando isto → roda aquilo. Nas próximas aulas ele fica mais esperto: formatar, avisar, barrar o perigoso.

Trilha 01 · Aula 2 · Iniciante

settings.json
a fundo

Onde os hooks moram, como cada um é montado, e o que o Claude entrega pro seu comando na hora de rodar.

role para estudar

01 Três lugares pros hooks

Um hook pode viver em três arquivos, e todos valem ao mesmo tempo:

  • Global~/.claude/settings.json. Vale pra todos os seus projetos.
  • Projeto.claude/settings.json. Vai pro Git; todo o time herda.
  • Local.claude/settings.local.json. Fica no .gitignore; é só seu.

Quando há conflito, o mais específico ganha: local sobrepõe projeto, que sobrepõe global. Regra prática: o que é do time vai no .claude/settings.json; o que é gosto seu vai no .local.json.

Teste-se

Qual arquivo é pessoal e NÃO vai pro Git?

02 Anatomia de um hook

Todo hook tem a mesma forma de bonecas russas: o evento guarda uma lista; cada item tem um matcher e uma lista de hooks; cada hook é um comando.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "./guarda.sh" }
        ]
      }
    ]
  }
}

Pode haver vários hooks no mesmo matcher — eles rodam em ordem. E vários matchers no mesmo evento. Comece com um; a estrutura aguenta crescer.

03 O matcher: onde o hook pega

Para PreToolUse e PostToolUse, o matcher casa com o nome da ferramenta. Três formas:

  • Nome exato"Bash" pega só o terminal.
  • Alternância"Edit|Write" pega edição ou escrita de arquivo.
  • Tudo"*" (ou vazio) pega qualquer ferramenta.

Eventos que não são de ferramenta (como SessionStart ou Stop) não usam matcher de ferramenta — eles simplesmente disparam no momento.

Teste-se

Para um hook pegar tanto Edit quanto Write, o matcher é:

04 O que o hook recebe

Na hora de rodar, o Claude entrega ao seu comando um JSON pela entrada padrão (stdin) com o contexto: qual evento, qual ferramenta, e a entrada dela.

# lê o comando que o Bash ia rodar
cmd=$(jq -r '.tool_input.command')
# lê o arquivo que foi editado
file=$(jq -r '.tool_input.file_path')

Também há a variável $CLAUDE_PROJECT_DIR apontando pra raiz do projeto — útil pra achar seus scripts. Com o stdin + jq, seu hook sabe exatamente o que está prestes a acontecer.

Indo mais fundo

O JSON traz mais campos (session_id, cwd, hook_event_name…). Você raramente precisa de todos — tool_name e tool_input resolvem a maioria dos hooks. A Aula 4 desta trilha destrincha esse JSON campo a campo.

Pratique agora 0/4 feito

Veja o que o hook recebe

Registre o JSON que o Claude entrega ao hook — e leia com seus olhos o tool_input. ~5 min, seguro (só observa).

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "cat >> .claude/edits.log" }
        ]
      }
    ]
  }
}

✓ Você viu o payload real. O matcher Edit|Write e o stdin deixam de ser abstração: é este JSON que o hook lê.

Trilha 01 · Aula 3 · Iniciante

Os eventos,
um por um

Do começo da sessão ao fim de uma tarefa: os momentos em que um hook pode ganchar — e quais deles, de verdade, conseguem barrar alguma coisa.

role para estudar

01 O ciclo de uma tarefa (linha do tempo)

Uma sessão do Claude Code tem um ciclo de vida, e cada volta dele abre uma janela pra um hook. Em ordem: a sessão começa, você envia uma mensagem, o Claude eventualmente usa ferramentas (uma de cada vez, antes e depois de cada uma), ele termina de responder, e — mais cedo ou mais tarde — a sessão acaba.

Pensar nos hooks como pontos numa linha do tempo ajuda mais do que decorar nomes soltos: cada evento é só "isto aconteceu agora" — e o resto da aula é ver, um por um, o que cada ponto permite fazer.

02 Antes e depois da ferramenta

Toda vez que o Claude usa uma ferramenta — Bash, Edit, Write, o que for — dois eventos a cercam:

  • PreToolUse — dispara antes de qualquer ferramenta rodar. Pode bloquear (exit 2); só recebe tool_name e tool_input, porque a ferramenta ainda não rodou.
  • PostToolUse — dispara depois que a ferramenta terminou com sucesso. Já recebe também o tool_response (o resultado). Um exit 2 aqui não bloqueia — a ferramenta já rodou.

É a mesma ferramenta, mas dois momentos com poderes diferentes: o Pre decide se roda; o Post reage a o que rodou.

Teste-se

Por que só o PreToolUse consegue impedir uma ferramenta de rodar?

03 Você fala, o Claude para

Nem todo evento é sobre ferramenta. Alguns marcam o vaivém da conversa:

  • UserPromptSubmit — dispara quando você envia uma mensagem.
  • Stop — dispara quando o Claude termina um turno. Também pode bloquear com exit 2 — não pra travar uma ferramenta, mas pra obrigar o Claude a continuar (ex.: "ainda falta atualizar o PLAN.md").
  • SubagentStop — o mesmo que Stop, mas para quando é um subagente que termina. Não bloqueia.

Repare a assimetria: Stop é uma das duas portas que realmente travam (a outra é o PreToolUse); UserPromptSubmit e SubagentStop não bloqueiam — eles observam ou injetam contexto.

Teste-se

Qual evento dispara quando VOCÊ envia uma mensagem, antes de o Claude processá-la?

04 Sessão e manutenção

Os últimos quatro cuidam do entorno da sessão, não de uma ferramenta ou mensagem específica:

  • SessionStart — quando a sessão começa. Sem matcher; ótimo pra injetar contexto (branch, commits, regras).
  • SessionEnd — quando a sessão termina normalmente. Não bloqueia, não modifica nada — serve pra limpeza, métricas, arquivar algo.
  • PreCompact — antes de o histórico da conversa ser compactado (resumido pra caber no contexto). É o evento menos documentado — dá pra usar pra guardar algo antes da compactação, mas sem contar com detalhes finos.
  • Notification — quando o Claude precisa de você (ficou parado, pede permissão, um MCP pergunta algo). Único desse grupo com matcher (permission_prompt, idle_prompt, auth_success, elicitation_dialog) — e pode até customizar a mensagem mostrada.
Indo mais fundo

SubagentStop e PreCompact são os dois eventos com documentação oficial mais incompleta — funcionam, mas espere menos previsibilidade fina do que em PreToolUse ou Stop. Teste sempre antes de confiar algo crítico a eles.

05 Quem pode barrar

Com oito nomes na cabeça, fica fácil achar que qualquer um deles trava alguma coisa saindo com exit 2. Não é assim:

  • PreToolUse — exit 2 barra a ferramenta antes dela rodar.
  • Stop — exit 2 barra o fim do turno (obriga o Claude a continuar).
  • Todos os outros — PostToolUse, Notification, SessionStart, SessionEnd, SubagentStop, PreCompact, UserPromptSubmit — um exit diferente de 0 é só erro não-bloqueante: a ação segue normalmente (fail-open). No PostToolUse em especial, nem faria sentido bloquear: a ferramenta já rodou.

Guarde essa regra: só Pre e Stop travam de verdade. Isso evita a armadilha de escrever uma trava num evento que nunca vai impedir nada.

Teste-se

Um hook PostToolUse sai com exit 2. O que acontece?

Pratique agora 0/4 feito

Um SessionStart que avisa data e branch

Faça a sessão imprimir onde e quando ela começou — a primeira coisa que roda, antes de qualquer ferramenta. ~5 min, seguro (só observa).

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "{ date; echo branch: $(git branch --show-current); } >> .claude/session.log" }
        ]
      }
    ]
  }
}

✓ SessionStart é a primeira janela do ciclo — sem matcher, sem depender de ferramenta nenhuma. É o lugar certo pra qualquer contexto que o Claude devia saber desde o primeiro segundo.

Trilha 01 · Aula 4 · Iniciante

O que o
hook recebe

Todo hook começa igual: um JSON entra pela entrada padrão. Aprenda a ler esse payload — os campos que sempre estão lá, e os que mudam com o evento.

role para estudar

01 Tudo chega por stdin (JSON)

Não importa o evento: quando um hook dispara, o Claude Code monta um objeto JSON com o contexto daquele momento e entrega pela entrada padrão (stdin) do seu comando. Seu script lê esse texto — com cat, jq, um json.load(sys.stdin) em Python — e decide o que fazer.

Uma consequência prática: hooks não podem ser interativos. Eles recebem tudo de uma vez, no início, e respondem de uma vez, no fim (por exit code ou por um JSON impresso). Não dá pra "perguntar" nada no meio do caminho.

02 Os campos comuns

Quatro campos aparecem em todo hook, não importa o evento:

  • session_id — identifica a sessão atual.
  • cwd — o diretório de trabalho no momento.
  • transcript_path — caminho pro arquivo com o histórico da conversa.
  • hook_event_name — diz qual evento é esse (útil se o mesmo script atende mais de um evento).

Pense neles como o envelope: chega sempre, e te orienta antes mesmo de olhar o resto do conteúdo.

Teste-se

Qual destes campos aparece em QUALQUER hook, seja qual for o evento?

03 tool_input e tool_response

Fora o envelope comum, o resto do JSON muda com o evento. Nos eventos de ferramenta, o par mais importante é:

  • tool_input — o que a ferramenta vai receber. Presente em PreToolUse e PostToolUse.
  • tool_response — o resultado depois da ferramenta rodar. Só existe em PostToolUse — em PreToolUse a ferramenta ainda nem rodou.

Outros eventos trazem seus próprios campos: Notification vem com notification_type e notification_message; SessionStart não traz nenhum dado de ferramenta (não tem o que trazer — nada rodou ainda). Sempre olhe o hook_event_name antes de assumir que um campo vai estar lá.

Teste-se

Por que o campo tool_response não aparece no JSON de um PreToolUse?

04 Ler com jq

Na prática, três comandos resolvem quase tudo:

# nome da ferramenta
jq -r '.tool_name'
# o comando que o Bash ia rodar
jq -r '.tool_input.command'
# o arquivo que foi editado
jq -r '.tool_input.file_path'

Pra explorar um payload inteiro enquanto você aprende, jq . (sem filtro) imprime tudo formatado e legível — bom pra debugar um hook novo antes de escrever o filtro certo.

Indo mais fundo

Sem jq instalado, dá pra ler o mesmo JSON em Python: import sys, json; data = json.load(sys.stdin), e depois data.get("tool_name"). Mais verboso, mas roda em qualquer máquina com Python.

Pratique agora 0/4 feito

Logue tool_name + command com jq

Grave, por extenso, qual ferramenta rodou e qual comando ela recebeu — direto do JSON do stdin. ~5 min, seguro (só observa).

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_name + \" \" + .tool_input.command' >> .claude/tool.log" }
        ]
      }
    ]
  }
}

✓ Você foi do payload bruto até um log legível com um filtro de uma linha. É essa a distância real entre "chegou um JSON" e "meu hook decide algo com ele".

Trilha 01 · Aula 5 · Iniciante

Como o hook
responde

Exit code é o sinal simples. Mas um hook também pode devolver um JSON — e explicar, com um motivo, a decisão que tomou.

role para estudar

01 Dois jeitos de responder

Um hook fala de volta com o Claude Code de duas formas, que podem se combinar:

  • Exit code — o sinal mais simples: um número. Rápido de escrever, cobre a maioria dos casos.
  • JSON no stdout — quando o número não basta. Você imprime um objeto com uma decisão explícita, um motivo em texto, e ajustes finos de comportamento.

Comece sempre pelo exit code — é o caminho rápido das aulas anteriores. Migre pro JSON quando precisar de nuance: pedir confirmação em vez de barrar, ou explicar por quê de um jeito mais rico do que uma linha no stderr.

02 Os exit codes

Três faixas de código, três comportamentos bem diferentes:

  • 0 — sucesso, segue normalmente.
  • exit 2 — bloqueia, mas em PreToolUse e Stop. O que você imprime no stderr vira o motivo.
  • 1, 3 ou mais — erro não-bloqueante: a ação segue (fail-open). Seu hook quebrou, mas o Claude não trava por causa disso.

Esse último ponto é deliberado: um hook malformado não devia conseguir travar o Claude Code inteiro. Prefira falhar aberto — se seu script tiver um bug, o pior cenário é o hook não fazer nada, não é o Claude ficar parado.

Indo mais fundo

Esse princípio se chama fail-open: no seu script, sempre tenha um caminho de saída garantido (um trap no shell, um try/except em Python) que devolve exit 0 se algo inesperado acontecer. Um hook de segurança que trava sozinho é pior do que nenhum hook.

Teste-se

Um hook Notification sai com exit 1 por um bug no script. O que acontece?

03 JSON de saída

Em vez de só um exit code, o hook pode imprimir um JSON no stdout com uma decisão mais expressiva:

{
  "decision": "approve|block",
  "reason": "string",
  "suppressOutput": false,
  "continue": true
}

Cada campo tem um papel:

  • decision"approve" ou "block", a versão explícita do exit code.
  • reason — o motivo, em texto — o que antes só ia pelo stderr.
  • suppressOutput — esconde a saída do hook do transcript, quando ela não interessa a ninguém.
  • continue — se false, para a sessão inteira, não só a ação atual. Use com cuidado.

Teste-se

Qual campo do JSON de saída explica, em texto, o motivo de uma decisão?

04 permissionDecision e contexto extra

Pra decisões de ferramenta, o JSON pode ficar ainda mais fino com um bloco próprio:

{
  "hookSpecificOutput": {
    "permissionDecision": "allow|deny|ask"
  },
  "reason": "confirmar antes de rodar em produção"
}

Três posturas, não duas: allow (libera), deny (barra — equivalente ao exit 2) e ask (pausa e pergunta a você, em vez de decidir sozinho). É essa terceira opção que o exit code puro não tem — nem tudo é preto no branco, e às vezes o certo é só avisar.

Alguns eventos também aceitam um additionalContext dentro do JSON — texto extra que entra no contexto do Claude, além da decisão em si. É como injetar uma nota de rodapé no que ele está processando.

Pratique agora 0/4 feito · capstone

Um PreToolUse que pergunta em vez de barrar

Feche a trilha fazendo um hook responder com permissionDecision: "ask" e um motivo — sem apagar nada. ~5 min, seguro: só pede confirmação.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command",
            "command": "grep -q PEDECONFIRMACAO && printf '{\"hookSpecificOutput\":{\"permissionDecision\":\"ask\"},\"reason\":\"confirmar antes de rodar\"}'; exit 0" }
        ]
      }
    ]
  }
}

✓ Fim da trilha: seu hook não só decide (exit code) — ele explica a decisão em JSON, com permissionDecision e um motivo. É o mesmo quando isto → roda aquilo da Aula 1, só que agora o Claude entende exatamente o porquê.