Dez receitas de hook prontas pra produção — uma por necessidade real, cobrindo as sete famílias de eventos de ciclo de vida. Copia, cola, customiza. Cada receita tem <20 linhas de bash.
O que é um hook, em 30 segundos
Um hook é um script de shell que o Claude Code roda em um evento específico do ciclo de vida. Ele lê um payload JSON do stdin, decide via exit code (0 = permite, 2 em evento Pre* = BLOQUEIA com stderr mostrado pro agent, qualquer outro = só loga), e opcionalmente escreve additionalContext no stdout pro agent consumir.
Anatomia de uma receita
Quando — o cenário real onde essa receita compensa.
Payload — JSON de exemplo que o hook recebe no stdin.
Script — o bash de verdade. Curto. Real.
Configuração — o fragmento de settings.json.
Pegadinha — a coisa que pegou alguém.
§1
Injetar CHANGELOG + estado não comitado no início da sessão
SessionStart
Quando — Claude abre uma sessão e o usuário quer as últimas notas de release e o estado atual do git apresentados automaticamente, em vez de perguntar "o que mudou?" toda vez.
Pegadinha — SessionStart dispara em todo source incluindo compact. Se você quer só startup, branche em source via jq -r '.source' primeiro.
§2
Bloquear saída se SESSION.md está desatualizado com WIP
Stop
Quando — você termina sessões com WIP que a próxima sessão esquece. O hook chia uma vez: "ou comita, ou atualiza SESSION.md, e aí termina normalmente."
▸ Script — .claude/hooks/session-stop.sh
#!/usr/bin/env bash
set -euo pipefail
dirty=$(git status --porcelain 2>/dev/null || true)
[[ -z "$dirty" ]] && exit 0
session_mtime=$(stat -c %Y .claude/SESSION.md 2>/dev/null || echo 0)
started=$(stat -c %Y .claude/.session-state/started-at 2>/dev/null || echo 0)
[[ "$session_mtime" -gt "$started" ]] && exit 0
[[ -f .claude/.session-state/nagged ]] && exit 0
touch .claude/.session-state/nagged
echo "Before ending: uncommitted WIP exists but SESSION.md was not updated this session. Update it (or commit), then end normally." >&2
exit 2
Pegadinha — o stdout de hooks UserPromptSubmit é injetado como additionalContext logo depois da mensagem do usuário. Mantém limitado (head -c 4096) ou um PR grande inunda a janela de contexto.
§4
Logar qual CLAUDE.md / regra carregou quando (debug)
InstructionsLoaded
Quando — você adicionou uma regra com escopo de path (frontmatter paths:) e parece que ela não está disparando. O hook escreve um trace JSONL pra você confirmar.
Pegadinha — InstructionsLoaded dispara dentro do mesmo turno do load que o triggou. Hook é observabilidade pura; stdout/stderr NÃO são injetados na conversa para esse evento.
§5
Bloquear formas destrutivas de shell
PreToolUse(Bash)
Quando — você confia no Claude pra rodar Bash, mas nunca quer ver rm -rf, git push --force, ou instalação de dependência sem lockfile. Bloqueia esses por forma, não por negação global.
Pegadinha — matcher: "Bash" no nível do array limita o hook a chamadas Bash. Sem isso, o hook dispara em toda chamada de tool e seu jq query falha em payload com forma errada.
§6
Exigir handoff de delegação com 5 campos
PreToolUse(Agent)
Quando — você sempre acaba com sub-agents under-specificados que inventam o próprio framing. Impõe um handoff estruturado: TASK / CONTEXT / CONSTRAINTS / DELIVERABLE / DONE_WHEN.
▸ Script — .claude/hooks/pre-agent.sh
#!/usr/bin/env bash
set -euo pipefail
prompt=$(jq -r '.tool_input.prompt')
required=(TASK CONTEXT CONSTRAINTS)
outcome=0
for f in "${required[@]}"; do
echo "$prompt" | grep -qiE "^[[:space:]]*$f:" || { missing+=" $f"; outcome=1; }
done
echo "$prompt" | grep -qiE "^[[:space:]]*(DELIVERABLE|DONE_WHEN):" || outcome=1
[[ $outcome -eq 0 ]] && exit 0
cat >&2 <<MSG
delegation-gate: missing fields$missing (and/or DELIVERABLE/DONE_WHEN).
Use 5-field handoff:
TASK: <one sentence>
CONTEXT: <files/paths>
CONSTRAINTS: <what NOT to do>
DELIVERABLE: <artifact> | DONE_WHEN: <verifier>
MSG
exit 2
Pegadinha — match case-insensitive (-i) é proposital; agents escrevem Task:, TASK:, task: indiferentemente. Ancorar no início da linha (^) evita match em "task list" dentro da prosa.
§7
Rodar prettier check em todo arquivo editado
PostToolUse(Edit | Write | MultiEdit)
Quando — você quer consistência de format sem prettier --write manual depois de cada edit. O hook roda --check e mostra violações pro agent no próximo turno.
Pegadinha — o matcher "Edit|Write|MultiEdit" usa alternação regex. Nomes de campos de payload diferem: Edit/MultiEdit usam file_path; Write usa file_path; algumas tools legadas usaram path. O fallback // empty no jq cobre todas as formas.
§8
Sugerir alternativa quando o usuário nega uma tool
PermissionDenied
Quando — o agent pede Bash(curl …), você nega, mas preferia que ele usasse outro verbo. O hook injeta uma dica pro agent tentar de novo com a tool certa.
▸ Script — .claude/hooks/perm-denied-hint.sh
#!/usr/bin/env bash
set -euo pipefail
denied=$(jq -r '.tool_name + "(" + (.tool_input | tostring) + ")"')
case "$denied" in
Bash*curl*) echo "hint: try the WebFetch tool instead of curl" ;;
Bash*rm*) echo "hint: this project blocks rm; if you need to remove, ask the user" ;;
Bash*gh*pr*) echo "hint: prefer the GitHub MCP server (mcp__github__*) for PR ops" ;;
esac
Pegadinha — PermissionDenied dispara depois do usuário negar o prompt. Stdout vira additionalContext no próximo turno do agent. Não moralize — dá um redirect concreto ou fica quieto.
§9
Verificar se o deliverable que o sub-agent disse ter feito existe
SubagentStop
Quando — sub-agents dizem "pronto, criei o arquivo X" mas X não existe. O hook parseia o campo DELIVERABLE: do brief, checa o path, e flagga discrepâncias.
▸ Script — .claude/hooks/subagent-verify.sh
#!/usr/bin/env bash
set -euo pipefail
brief=$(jq -r '.tool_input.prompt // empty')
deliverable=$(echo "$brief" | grep -iE '^[[:space:]]*DELIVERABLE:' | head -1 | sed -E 's/^[^:]*:[[:space:]]*//')
# crude: pull any path-shaped token from the deliverable line
path=$(echo "$deliverable" | grep -oE '[a-zA-Z0-9_./-]+\.(ts|tsx|md|json|astro|sh|py|go|rs)' | head -1 || true)
[[ -z "$path" ]] && exit 0
[[ -f "$path" ]] && exit 0
echo "subagent-advisory: claimed deliverable '$path' not found in tree" >&2
exit 0 # advisory
Pegadinha — extrair paths de prosa é frágil; o script só pega deliverables que nomeiam um path com extensão reconhecida. Pra checks mais ricos, parea DELIVERABLE com DONE_WHEN: e roda o comando em vez disso.
§10
Snapshot dos últimos 12 turnos antes da sumarização
PreCompact
Quando — sessões longas são sumarizadas e o sumarizador descarta wording verbatim, paths de arquivo, identificadores exatos. O hook salva turnos brutos no disco pro próximo session-start re-injetar.
▸ Script — .claude/hooks/pre-compact.sh
#!/usr/bin/env bash
set -euo pipefail
transcript=$(jq -r '.transcript_path // empty')
[[ -f "$transcript" ]] || exit 0
# extract last 12 user prompts + their assistant text
jq -c 'select(.role=="user" or .role=="assistant") | {role, content}' "$transcript" \
| tail -n 48 > .claude/COMPACT_NOTES.md
echo "wrote .claude/COMPACT_NOTES.md ($(wc -l < .claude/COMPACT_NOTES.md) lines)" >&2
▸ Hook companheiro SessionStart
# in .claude/hooks/session-start.sh
if [[ "$(jq -r '.source')" = "compact" && -f .claude/COMPACT_NOTES.md ]]; then
echo "=== PRE-COMPACT SNAPSHOT ==="
cat .claude/COMPACT_NOTES.md
fi
Pegadinha — só PreCompact pode escrever antes do sumarizador truncar contexto, e só SessionStart com source: "compact" pode injetar depois. Nenhum outro hook ponteia esse gap. As duas metades são necessárias.