Hooks do Claude Code / Trilha 04
Trilha 04 · Equipe & CI
O que muda quando o hook sai da sua máquina: versionar pro time, confiar no determinismo, rodar sem ninguém olhando (headless/CI) e a higiene pra não se machucar com o poder que um hook tem.
Aulas desta trilha
settings.json no Git, o time inteiro herda as mesmas regras, o que fica de fora (local) e a ordem de precedência.
Idempotência, timeouts, testar um hook isolado com stdin de mentira e falhar aberto com log pra depurar.
Modo headless, hooks em CI/pipeline, exit codes sem interação e o que acontece quando um subagente termina.
Hook roda com todos os seus poderes: nunca confie no input, cuidado no PreCompact e o checklist final de segurança.
Trilha 04 · Aula 1 · Avançado
Um hook que só existe na sua máquina não protege ninguém além de você — é hora de fazer ele viajar com o repositório.
↓ role para estudar
Até aqui, todo hook que você montou vivia na sua máquina. Mas o .claude/settings.json é um arquivo normal, dentro da pasta do projeto — e um arquivo normal pode ir pro Git.
Quando ele é versionado (commitado, com git add), deixa de ser "o hook do Nei" e vira o hook do projeto. Ele passa a viajar junto com o código, no mesmo commit que a trava ou o formatador que ele protege.
Teste-se
Pra um hook deixar de ser só seu e passar a ser do projeto, o que muda?
Com o settings.json versionado, o efeito é automático: quem clona o repositório, ou dá git pull, recebe o arquivo — e o Claude Code lê os mesmos hooks. Sem pedir pra ninguém instalar nada, sem colar instrução em nenhum README.
Isso muda a régua do que vale a pena automatizar. Um formatador ou uma trava de segurança que só você tem protege só você. O mesmo hook, versionado, protege todo mundo que tocar o código — inclusive quem ainda nem entrou no time.
Teste-se
Se um colega clona o repositório e o .claude/settings.json já tem uma trava configurada, o que acontece?
Nem tudo é do time. Segredos, atalhos pessoais, um caminho que só existe na sua máquina — isso não deveria viajar pro repositório. Pra isso existe o settings.local.json: mesmo formato, mesma pasta .claude/, mas listado no .gitignore.
# fica de fora do repositório
echo ".claude/settings.local.json" >> .gitignore
Regra prática: se um hook deveria valer pra qualquer pessoa que abrir esse projeto, ele vai no settings.json. Se é gosto seu — ou tem algo sensível — ele vai no .local.json.
Teste-se
Você quer um ajuste pessoal, só pra ESTE projeto, sem ir pro Git. Qual arquivo?
Você agora tem até três arquivos valendo ao mesmo tempo: o global (~/.claude/settings.json, todos os seus projetos), o de projeto (.claude/settings.json, versionado) e o local (.claude/settings.local.json, só seu, só aqui).
Em conflito, o mais específico ganha: local sobrepõe projeto, que sobrepõe global. Pensa assim: o global é a régua geral da sua casa; o de projeto é a regra do prédio; o local é o post-it na sua porta — e o post-it vence.
Teste-se
Em conflito entre .claude/settings.json (projeto) e .claude/settings.local.json (local), quem vence?
Isso vale ferramenta por ferramenta, não é tudo-ou-nada: se o global tem um hook em PostToolUse e o de projeto tem outro em PreToolUse, os dois convivem — a precedência decide quem manda quando há conflito de verdade na mesma regra.
Pratique agora 0/4 feito
Faça o padrão do time viajar com o repositório de verdade. ~5 min, seguro: você só está versionando arquivo de configuração, não rodando nenhum comando destrutivo.
# o que é do time vai pro Git git add .claude/settings.json git status # o que é só seu fica de fora echo ".claude/settings.local.json" >> .gitignore git add .gitignore git commit -m "chore: versiona hooks do time"
✓ O padrão deixou de morar só na sua cabeça (ou só na sua máquina) — agora ele mora no repositório, e viaja com ele.
Trilha 04 · Aula 2 · Avançado
Um hook que às vezes trava e às vezes não é pior que não ter hook nenhum — aqui é onde você aprende a confiar (e a testar) o que automatizou.
↓ role para estudar
Um hook é um comando comum, e comando comum não "acha" nada — ele é idempotente: rodar a mesma entrada duas vezes tem que dar o mesmo resultado, sem quebrar nada na segunda vez.
Exemplo prático: um hook que formata um arquivo pode rodar dez vezes seguidas — o arquivo formatado, formatado de novo, continua igual. Já um hook que soma algo a cada execução (tipo incrementar um contador sem checar se já rodou) não é idempotente — e é aí que moram os bugs sutis de automação.
Teste-se
Um hook idempotente significa que:
timeoutUm hook entra no caminho de cada ação — se ele travar, tudo trava junto. Por isso cada hook tem um timeout, em segundos:
{ "hooks": { "PreToolUse": [ {
"matcher": "Bash",
"hooks": [ { "type": "command", "command": "./guarda.sh",
"timeout": 10 } ]
} ] } }
Se você não define timeout, o padrão de um hook de comando é 600 segundos (10 minutos) — generoso demais pra uma trava. Um hook rápido nem chega perto; mas um hook que faz chamada de rede sem cuidado pode pendurar sua sessão até o corte. Por isso vale definir um limite curto (ex.: 10) nos hooks que entram no caminho de cada ação.
Teste-se
Se você não define o campo timeout num hook de comando, qual é o padrão?
Você não precisa do Claude Code de verdade pra saber se um hook funciona. Todo hook lê o mesmo stdin — então você simula esse JSON na mão e alimenta o hook direto:
# teste isolado — sem abrir o Claude Code
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
| bash .claude/hooks/guarda.sh
echo "Exit: $?"
Isso vale ouro: você vê exatamente o que o hook faria, no seu terminal, em segundos — sem esperar uma sessão inteira só pra descobrir que esqueceu uma aspa.
Um hook que quebra por um erro que você não previu não deveria travar seu trabalho todo. A regra é falhar aberto: capture o erro e deixe seguir.
# captura qualquer erro inesperado e libera set +e trap 'exit 0' ERR # ... lógica do hook ...
E pra depurar quando algo dá errado, rode com -x (mostra cada linha executada) alimentando um arquivo com o JSON de teste:
bash -x .claude/hooks/guarda.sh < entrada.json echo "Exit: $?"
Teste-se
Como testar um hook sem precisar que o Claude dispare o evento de verdade?
"Falhar aberto" (exit 0 em erro inesperado) é diferente de "falhar barrando" (exit 2 de propósito). O primeiro é rede de segurança contra bug no seu script; o segundo é a trava que você desenhou pra travar mesmo.
Pratique agora 0/4 feito
Alimente um hook com um JSON falso e confirme que ele se comporta igual duas vezes. ~5 min, seguro: nada disso passa perto do Claude Code de verdade.
#!/usr/bin/env bash # .claude/hooks/testa.sh set +e trap 'exit 0' ERR INPUT=$(cat) CMD=$(printf '%s' "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null) echo "recebi: $CMD" >> .claude/test.log exit 0
✓ Você testou um hook sem precisar de uma sessão inteira do Claude Code — e viu, com os próprios olhos, o determinismo funcionando.
Trilha 04 · Aula 3 · Avançado
Ninguém está na tela — nem por isso os hooks tiram folga: eles seguram a régua também no modo headless e no CI.
↓ role para estudar
claude -pO Claude Code também roda sem interface nenhuma — o modo headless, disparado por script: claude -p "seu comando aqui". Ele lê o prompt, trabalha, responde e termina. Nada de tela, nada de você olhando.
É esse modo que entra num script de deploy, num cron, ou dentro de um pipeline. E é justamente aí que os hooks provam o próprio valor.
Se o settings.json está versionado (Aula 1), ele vale em qualquer lugar que rode o Claude Code — inclusive dentro de um runner de CI, sem ninguém logado, sem tela nenhuma.
A trava que barra rm -rf, o formatador que ajeita o código, o log que registra os comandos — tudo isso continua ativo. O mesmo padrão que protege você no dia a dia protege o robô também.
Teste-se
Em modo headless (claude -p) e dentro de um pipeline de CI, os hooks configurados:
Sem ninguém na tela, o hook não pode parar pra perguntar. Hooks não são interativos: nada de esperar um humano digitar "sim" no meio da execução. Tudo se resolve pelo PreToolUse e pelo código de saída:
PreToolUse e Stop); o stderr vira o motivo.Em CI isso é ainda mais importante: se um hook tentasse "perguntar", o pipeline travaria pra sempre esperando uma resposta que nunca chega.
Teste-se
Um hook pode pausar a execução e esperar você digitar uma resposta no meio do processo?
Além do exit code, um PreToolUse pode devolver JSON com permissionDecision: allow/deny/ask. Mas ask não faz sentido em headless — não tem quem responda. Em CI, prefira sempre allow/deny explícitos.
Existe um evento parecido com Stop, mas pra um nível abaixo: SubagentStop dispara quando um subagente termina o próprio turno — não o agente principal, e sim uma tarefa delegada.
Diferenças importantes: SubagentStop não bloqueia (ao contrário de Stop, que aceita exit 2), e não usa matcher. Trate-o como um aviso ("esse pedaço terminou"), não como uma trava — e não construa lógica crítica só nele, já que seu comportamento pode chegar incompleto.
Teste-se
Qual evento dispara quando um SUBAGENTE (não o agente principal) termina seu turno?
Pratique agora 0/4 feito
Prove que o hook dispara mesmo sem tela nenhuma. ~5 min, seguro: o comando só lista arquivos.
# o hook de log que você já tem em .claude/settings.json continua valendo cat .claude/settings.json # rode o Claude Code sem interface nenhuma claude -p "liste os arquivos deste diretório" # confira: o hook disparou mesmo em modo headless cat .claude/test.log
✓ O mesmo hook que protege o seu dia a dia protegeu a automação — sem nenhuma linha extra de configuração.
Trilha 04 · Aula 4 · Avançado
Um hook roda com todo o seu poder — e poder sem cuidado é a receita certa pra um acidente caro.
↓ role para estudar
Um hook não é uma sandbox nem um script "de brincadeira" — ele roda com os mesmos poderes do seu usuário: o mesmo shell, as mesmas credenciais, o mesmo acesso à rede que você tem quando abre o terminal.
Isso não é um defeito, é a razão de hooks funcionarem: pra formatar um arquivo ou barrar um rm -rf, o hook precisa poder tocar no sistema de verdade. Mas o mesmo poder que protege pode, mal escrito, estragar — então trate hook com o respeito que um script de acesso total merece.
Teste-se
Um hook, ao rodar, tem acesso a:
O tool_input que chega no seu hook vem de um contexto que você não controla totalmente. Jogar esse valor direto dentro de um comando shell é a porta de entrada clássica pra injeção de shell:
# perigoso: interpola direto no shell cmd=$(jq -r '.tool_input.command') eval "$cmd" ← NÃO faça isso # melhor: trate como dado, valide antes de agir cmd=$(jq -r '.tool_input.command') echo "$cmd" | grep -qE 'rm -rf|:\(\)\{' && { echo "bloqueado" >&2; exit 2; } exit 0
O mesmo cuidado vale pra caminho de arquivo: sempre valide (sem ../ saindo da pasta do projeto) antes de ler ou escrever — path traversal é o mesmo problema, só que com arquivo em vez de comando. E proteja de propósito o que é sensível: .env, .ssh, .git, chaves privadas.
Teste-se
Por que NUNCA se deve interpolar tool_input direto dentro de um comando shell (tipo eval "$cmd")?
Existe um evento chamado PreCompact, ligado ao momento em que o contexto da conversa é compactado. A ideia é dar uma brecha pra você persistir ou limpar estado antes disso acontecer.
Mas é um dos eventos menos documentados oficialmente — não assuma garantias fortes sobre exatamente quando ou como ele dispara. Na prática: use-o pra coisas não-críticas (um log, um backup), sempre com o mesmo cuidado de falhar aberto, e nunca aposte a segurança do projeto só nele.
Antes de considerar um hook "pronto pro time", passe por esta lista:
timeout definido.eval de string crua, caminhos validados..env, .ssh, .git, chaves fora do alcance de um hook mal escrito.settings.json (fica em variável de ambiente ou arquivo).exit 0 com log, não trava o Claude.Comece pequeno, endureça com o tempo. O hook que sobrevive ao seu time é o que ninguém nota — porque nunca atrapalha e sempre protege.
Vale revisar hooks de tempos em tempos como quem revisa permissão de acesso: o que era necessário há 3 meses pode não ser mais, e cada linha de comando automático é uma linha que roda sem você olhar — a essência desta trilha inteira.
Pratique agora 0/4 feito
Revise o settings.json do projeto contra o checklist de segurança — aspas certas, validação, sem segredo no comando. ~10 min, seguro: é revisão, não execução de nada destrutivo.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/guarda.sh\"",
"timeout": 10
}
]
}
]
}
}✓ Checklist fechado: aspas certas, sem segredo no comando, timeout definido, script falha aberto. Isso é maturidade — hook confiável é hook que nunca te machuca. Fim da trilha. 🎉