INEMA.CLUBPROHooks do Claude Code

Hooks do Claude Code / Trilha 03

Trilha 03 · Automação

Regras que rodam
sozinhas

Quatro hooks prontos pra colar: formatar ao salvar, avisar quando terminar, registrar cada comando e injetar contexto assim que a sessão abre.

4aulas
~36minutos
intermediárionível

Trilha 03 · Aula 1 · Intermediário

Formatar
ao salvar

Depois que o Claude edita um arquivo, um hook roda o formatador nele — sem você lembrar, sem exceção.

role para estudar

01 PostToolUse em Edit|Write

O hook mora no evento PostToolUse: ele dispara depois que o Claude termina de editar ou escrever um arquivo — a ação já aconteceu, é hora de arrumar a casa.

O matcher filtra em quais ferramentas o hook pega: "Edit|Write" pega as duas — edição de um arquivo existente ou escrita de um novo.

{ "hooks": { "PostToolUse": [ {
  "matcher": "Edit|Write",
  "hooks": [ { "type": "command",
    "command": "f=$(jq -r .tool_input.file_path); prettier --write \"$f\"" } ]
} ] } }

Leia de dentro pra fora: no evento PostToolUse, quando a ferramenta for Edit ou Write, roda o comando. Simples — e some da sua cabeça pra sempre.

Teste-se

Qual evento roda DEPOIS de uma ferramenta terminar de rodar?

02 Pegar o arquivo

O hook não sabe de antemão qual arquivo foi tocado — ele lê no JSON do stdin, campo tool_input.file_path. Com jq, isso vira uma variável de shell:

# lê o arquivo que acabou de ser editado
file=$(jq -r '.tool_input.file_path')

É esse $file que vai pro formatador na próxima seção. Sem ele, o hook não saberia em cima de qual arquivo agir.

03 Rodar o formatador

Com o caminho em mãos, escolha o formatador pela extensão do arquivo. Um case resolve pra qualquer stack:

case "$file" in
  *.js|*.ts|*.jsx|*.tsx) prettier --write "$file" ;;
  *.py) black -q "$file" ;;
  *.go) gofmt -w "$file" ;;
esac

Troque pelos formatadores do seu projeto. O ponto não é qual ferramenta — é que ela roda sempre, em todo arquivo, sem exceção humana.

Teste-se

Pra formatar um arquivo .py, qual linha do case entra em ação?

04 Silencioso e idempotente

Um hook de formatação não deve bloquear nada: sempre exit 0. E cuidado com uma pegadinha — no PostToolUse, um exit 2 não desfaz a edição (a ferramenta já rodou); ele só reporta um erro.

Pra não poluir o contexto quando tudo deu certo, use suppressOutput:

{ "suppressOutput": true }

E o formatador precisa ser idempotente: rodar duas vezes sobre o mesmo arquivo não pode quebrar nada — mesma entrada, mesmo resultado, sempre.

Indo mais fundo

PreToolUse e Stop conseguem barrar algo com exit 2. No PostToolUse, esse mesmo código é tratado como erro não-bloqueante — a ação já ocorreu, não tem como voltar atrás.

Teste-se

Se um hook PostToolUse sai com exit 2, o que acontece?

Pratique agora 0/4 feito

Prettier no arquivo recém-editado

Faça o Claude formatar sozinho todo arquivo que ele editar. ~5 min, seguro: só formata, não bloqueia nada.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "f=$(jq -r '.tool_input.file_path'); command -v prettier >/dev/null && prettier --write \"$f\" 2>/dev/null; exit 0" }
        ]
      }
    ]
  }
}

✓ Fechamento: o código sai sempre limpo — o formatador virou parte do fluxo, não uma tarefa manual que dá pra esquecer.

Trilha 03 · Aula 2 · Intermediário

Avisar quando
terminar

Dois eventos avisam na hora certa: Stop quando o Claude termina de responder, Notification quando ele precisa de você.

role para estudar

01 Stop e Notification

Dois eventos avisam de coisas diferentes. Stop dispara quando o Claude termina de responder — o turno acabou. Já Notification dispara quando ele precisa de você: ficou parado 60s+, pediu uma permissão, ou um MCP está esperando uma resposta.

A diferença importa: Stop é "acabei"; Notification é "estou esperando você". Cada um pede um tipo de aviso diferente.

Teste-se

Qual evento dispara quando o Claude precisa de uma permissão sua (ou fica parado 60s+)?

02 Disparar notify-send / um som

Pendure um aviso de desktop no evento que interessa. No Linux, notify-send resolve:

# Stop → notificação de desktop (linux)
notify-send "Claude Code" "terminou de responder"

Um som curto (paplay, afplay) também funciona, e é mais discreto que um popup. O importante: você para de vigiar o terminal.

03 Só quando importa

O evento Notification dispara por vários motivos — notification_type diz qual. Se você notificar em todos, vira spam. Filtre pelo que realmente precisa de você:

type=$(jq -r '.notification_type')
[ "$type" = "permission_prompt" ] || exit 0
notify-send "Claude Code" "precisa de permissão" --urgency=critical

Assim só o pedido de permissão dispara o alerta crítico — o resto (idle, elicitação) você decide se quer ou não, sem tocar em tudo o tempo todo.

Teste-se

Pra notificar só quando for pedido de permissão (e ignorar o resto), você filtra por:

04 Variações por SO

O comando de notificação muda com o sistema. Um case em cima de uname cobre os três:

case "$(uname)" in
  Linux)  notify-send "Claude Code" "terminou" ;;
  Darwin) osascript -e 'display notification "terminou" with title "Claude Code"' ;;
  *)      powershell.exe -Command "New-BurntToastNotification -Text 'Claude Code','terminou'" ;;
esac

No Windows (via WSL), chamar o powershell.exe de dentro do Linux costuma ser o caminho mais direto.

Indo mais fundo

Prefere som a popup? Troque o notify-send/osascript por paplay algum.wav (Linux) ou afplay algum.wav (macOS) — mais discreto, funciona igual como sinal de "terminou".

Pratique agora 0/4 feito

Notificação de desktop no evento Stop

Deixe o Claude te avisar com um popup toda vez que terminar de responder. ~5 min, seguro (só observa, não bloqueia).

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "command -v notify-send >/dev/null && notify-send 'Claude Code' 'terminou de responder'; exit 0" }
        ]
      }
    ]
  }
}

✓ Fechamento: você não fica mais vigiando o terminal — o aviso vem até você.

Trilha 03 · Aula 3 · Intermediário

Registrar
os comandos

PreToolUse em Bash grava cada comando com hora — um diário de tudo que a sessão rodou, sem bloquear nada.

role para estudar

01 PreToolUse em Bash

O evento PreToolUse dispara antes de qualquer ferramenta executar. Com matcher "Bash", ele pega só os comandos de terminal — antes de eles rodarem de verdade.

Aqui não vamos bloquear nada: o hook só observa e deixa seguir. É um registro, não uma trava.

Teste-se

Um hook PreToolUse com matcher "Bash" dispara quando?

02 Anexar num log com timestamp

Cada linha do log carrega a hora em que o comando ia rodar. Monte o carimbo com date:

# .claude/hooks/log-cmd.sh (PreToolUse, matcher "Bash")
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)

Sozinho isso não faz nada ainda — falta pegar o comando em si, que vem na próxima seção.

03 Extrair o command com jq

O texto do comando vem no JSON do stdin, campo tool_input.command. Junte com o timestamp e anexe num arquivo:

cmd=$(jq -r '.tool_input.command')
printf '%s\t%s\n' "$ts" "$cmd" >> "$CLAUDE_PROJECT_DIR/.claude/cmd.log"
exit 0

$CLAUDE_PROJECT_DIR garante que o log vai sempre pro mesmo lugar, não importa de onde o comando foi disparado. E exit 0 — o hook nunca barra, só grava.

Teste-se

Onde vem o texto do comando que o Bash vai rodar?

04 Auditoria simples do que rodou

O cmd.log vira um diário: todo comando, com hora, em ordem. Útil pra revisar o que uma sessão fez, investigar um comando estranho, ou reconstruir uma linha do tempo depois.

Um cuidado: hooks rodam com permissão total do seu usuário. Se um comando tiver um token ou senha embutido, ele vai parar no log — não deixe esse arquivo sair do .gitignore nem circular por aí.

Indo mais fundo

O log é só leitura pra você depois — o hook em si nunca deve bloquear com base nele. Quem bloqueia é uma trava separada (aula anterior da trilha); este hook aqui só observa.

Teste-se

Este hook de log deve bloquear o comando antes de ele rodar?

Pratique agora 0/4 feito

Log datado de todo comando

Grave, com hora, cada comando de Bash que o Claude rodar nesta sessão. ~5 min, seguro (só observa, sai sempre com exit 0).

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r '(now|gmtime|strftime(\"%Y-%m-%d %H:%M:%S\")) + \" \" + .tool_input.command' >> \"$CLAUDE_PROJECT_DIR/.claude/cmd.log\"; exit 0" }
        ]
      }
    ]
  }
}

✓ Fechamento: agora existe um diário datado de tudo que a sessão rodou — auditável, sem bloquear nada.

Trilha 03 · Aula 4 · Intermediário

Injetar contexto
no começo

SessionStart e UserPromptSubmit entregam contexto direto pro Claude — branch, git status, regras do projeto — sem você colar nada.

role para estudar

01 SessionStart e UserPromptSubmit

SessionStart dispara uma vez, quando a sessão abre. UserPromptSubmit dispara a cada mensagem que você envia. Nenhum dos dois é evento de ferramenta — não usam matcher, simplesmente disparam no momento certo.

Os dois podem devolver contexto extra pro Claude. A diferença é a frequência: um contexto de sessão (branch, regras do projeto) vai no SessionStart; algo que muda a cada mensagem entraria no UserPromptSubmit — com moderação, pra não repetir toda hora.

Teste-se

Qual evento dispara UMA VEZ, no início da sessão?

02 additionalContext dentro de hookSpecificOutput

Pra entregar contexto de verdade, o hook devolve JSON estruturado no stdout, com o texto dentro de hookSpecificOutput, campo additionalContext:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Branch: main\nMudanças: 2 arquivos"
  }
}

É esse texto — e só ele — que chega ao Claude como contexto extra. Nada de imprimir solto no stdout esperando que "apareça"; o formato importa.

Teste-se

Onde fica o texto que o hook injeta como contexto pro Claude?

03 Injetar git status / CLAUDE.md / TODO

Combine as fontes que importam: branch atual, o que mudou, talvez uma linha dizendo que existe um CLAUDE.md com regras. Monte a string e escape com jq -Rs pra virar um valor JSON válido:

# .claude/hooks/session-context.sh (SessionStart)
branch=$(git branch --show-current)
status=$(git status --short)
ctx="Branch: $branch"$'\n'"Mudanças:"$'\n'"$status"
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}' \
  "$(printf '%s' "$ctx" | jq -Rs .)"

O jq -Rs . lê texto puro (não JSON) e devolve uma string JSON já escapada — quebras de linha e aspas incluídas. Sem isso, um \n solto quebraria o JSON de saída.

04 Contexto certo e curto

A tentação é jogar o projeto inteiro no additionalContext. Resista: contexto demais polui e desperdiça espaço que o Claude usaria pra pensar no seu pedido. Injete só o que muda a decisão dele agora — branch, o que está sujo, uma ou duas linhas de regra — não o histórico inteiro.

Regra prática: se você não colaria aquilo manualmente numa mensagem, não injete automaticamente também.

Indo mais fundo

Combine os quatro hooks da trilha e o ambiente cuida de si mesmo: formata ao salvar, avisa quando termina, registra o que rodou, e já entrega o contexto certo assim que a sessão abre.

Teste-se

Por que não jogar o conteúdo inteiro do projeto no additionalContext sempre?

Pratique agora 0/4 feito

SessionStart injeta branch + git status · capstone

Deixe a sessão abrir já sabendo o branch e o que mudou no projeto — sem você contar nada. ~8 min.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          { "type": "command", "command": "bash .claude/hooks/session-context.sh" }
        ]
      }
    ]
  }
}

✓ Capstone: a sessão abre já sabendo do projeto — branch, mudanças, tudo isso sem você colar nada. Fim da trilha de automação.