INEMA.CLUBPROHooks do Claude Code

Hooks do Claude Code / Trilha 04

Trilha 04 · Equipe & CI

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.

4aulas
~36minutos
avançadonível

Trilha 04 · Aula 1 · Avançado

Versionar
pro time

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

01 settings.json no Git

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?

02 O time inteiro herda as mesmas regras

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?

03 settings.local.json: fora do Git

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?

04 Precedência: user → project → local

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?

Indo mais fundo

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

Comite o settings.json do projeto

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

Determinismo:
dá pra confiar

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

01 Idempotência: rodar duas vezes dá o mesmo

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:

02 Timeouts: o campo timeout

Um 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?

03 Testar isolado: stdin de mentira

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.

04 Fail-open + logar pra depurar

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?

Indo mais fundo

"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

Teste um hook com stdin de mentira

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

Rodando sem
ninguém olhando

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

01 Modo headless: claude -p

O 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.

02 Hooks em CI/pipeline

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:

03 Exit codes e não-interatividade

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:

  • exit 0 — segue normal.
  • exit 2 — barra (só vale em PreToolUse e Stop); o stderr vira o motivo.
  • exit 1, 3+ — erro não-bloqueante; o Claude segue (fail-open).

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?

Indo mais fundo

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.

04 SubagentStop: quando um subagente termina

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

Rode o Claude em -p com um hook ativo

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

Higiene:
hook é poder

Um hook roda com todo o seu poder — e poder sem cuidado é a receita certa pra um acidente caro.

role para estudar

01 Hooks rodam com TODOS os seus poderes

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:

02 Nunca confie no input

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")?

03 PreCompact e limpeza de estado

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.

04 Checklist de segurança final

Antes de considerar um hook "pronto pro time", passe por esta lista:

  • Rápido — não trava o fluxo; tem timeout definido.
  • Idempotente — rodar duas vezes não quebra nada.
  • Nunca confia no input — sem eval de string crua, caminhos validados.
  • Protege o sensível.env, .ssh, .git, chaves fora do alcance de um hook mal escrito.
  • Sem segredo no comando — nada de token/senha escrito direto no settings.json (fica em variável de ambiente ou arquivo).
  • Falha aberto — erro inesperado vira 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.

Indo mais fundo

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

Endureça seu settings.json · capstone

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. 🎉