Hooks do Claude Code / Trilha 03
Trilha 03 · Automação
Quatro hooks prontos pra colar: formatar ao salvar, avisar quando terminar, registrar cada comando e injetar contexto assim que a sessão abre.
Aulas desta trilha
PostToolUse pega o arquivo recém-editado e roda o formatador — prettier, black, gofmt — sem você lembrar.
Stop e Notification disparam o aviso certo na hora certa — sem vigiar o terminal esperando o Claude acabar.
PreToolUse em Bash grava cada comando com hora num log — um diário simples de auditoria.
SessionStart entrega branch, git status e as regras do projeto assim que a sessão abre. Capstone da trilha.
Trilha 03 · Aula 1 · Intermediário
Depois que o Claude edita um arquivo, um hook roda o formatador nele — sem você lembrar, sem exceção.
↓ role para estudar
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?
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.
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?
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.
Só 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
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
Dois eventos avisam na hora certa: Stop quando o Claude termina de responder, Notification quando ele precisa de você.
↓ role para estudar
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+)?
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.
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:
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.
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
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
PreToolUse em Bash grava cada comando com hora — um diário de tudo que a sessão rodou, sem bloquear nada.
↓ role para estudar
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?
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.
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?
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í.
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
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
SessionStart e UserPromptSubmit entregam contexto direto pro Claude — branch, git status, regras do projeto — sem você colar nada.
↓ role para estudar
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?
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?
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.
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.
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
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.