TIPS 05 · GUARDIA

O guardião

Dois hooks para o Claude Code: um pede-te confirmação antes de um comando apagar ficheiros e bloqueia os irreparáveis, o outro corre os testes depois de cada alteração.

O que é e porque importa

Estes são os dois hooks que uso nos meus projetos. Um hook é um comando que o Claude Code corre sozinho num momento preciso da sessão. A diferença em relação a uma instrução escrita no chat ou no CLAUDE.md está toda aqui: uma instrução, o Claude pode perdê-la pelo caminho, depois de duas horas de trabalho. Um hook não. É o Claude Code que o executa sempre, quer o Claude se lembre quer não.

O guardião serve porque alguns comandos não se anulam: apagam ficheiros sem passar pelo lixo, reescrevem o histórico do git, esvaziam uma tabela da base de dados. Basta um caminho errado ou um pedido mal percebido.

O meu guardião faz duas coisas. Se um comando apaga ficheiros, pede-te confirmação, também no modo auto, aquele em que o Claude trabalha sem te perguntar nada. Se for irreparável, como apagar o disco todo ou forçar um push, bloqueia-o e pronto.

Os testes depois de cada alteração servem para o outro problema clássico: o Claude muda um ficheiro, diz-te «feito», e entretanto partiu alguma coisa noutro sítio. Com o hook, o resultado dos testes chega-lhe logo, enquanto ainda tem na cabeça o que mexeu.

1. O guardião: antes de cada comando

O primeiro hook usa o evento PreToolUse, que dispara antes de o Claude usar uma ferramenta. Com o filtro «Bash», só olha para os comandos do terminal. Cria o ficheiro .claude/hooks/guardiao.sh na pasta do projeto e cola lá isto:

O ficheiro
#!/bin/bash
# guardiao.sh: pergunta antes de apagar, bloqueia o que é irreparável
command -v jq >/dev/null || { echo "Bloqueado: falta o jq (brew install jq)" >&2; exit 2; }
CMD=$(jq -er '.tool_input.command') || { echo "Bloqueado: input ilegível" >&2; exit 2; }
NO='rm[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+(/|~/?|\$HOME/?)\*?([[:space:]]|$)|git[[:space:]]+push([[:space:]].*)?[[:space:]](-f|--force)|git[[:space:]]+reset[[:space:]]+--hard|drop[[:space:]]+(table|database)'
if grep -Eiq "$NO" <<<"$CMD"; then echo "Bloqueado: comando irreparável ($CMD)" >&2; exit 2; fi
ASK='^[[:space:]]*((sudo|xargs)[[:space:]]+)*(rm|rmdir|unlink)([[:space:]]|$)|^[[:space:]]*find[[:space:]].*[[:space:]]-delete'
if tr ';&|()`' '\n\n\n\n\n\n' <<<"$CMD" | grep -Eq "$ASK"; then
  jq -n --arg r "O guardião: este comando apaga ficheiros. Confirmas?" \
    '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: $r}}'
fi
exit 0

Como funciona: antes de cada comando, o Claude Code passa ao script o comando em JSON, no campo tool_input.command. O script pode responder de três maneiras:

  • Sai com código 2: o comando não arranca e o Claude lê o motivo. Acontece com os comandos da lista NO.
  • Escreve um pequeno JSON com permissionDecision «ask» e sai com 0: o Claude Code mostra-te o pedido de permissão, com o motivo escrito pelo script, e o comando só arranca se disseres que sim. Acontece com os comandos da lista ASK.
  • Sai com 0 sem escrever nada: o comando segue em frente com as permissões normais.

As duas linhas depois do comentário bloqueiam tudo se faltar o jq, o programa que lê o JSON, ou se o input não se conseguir ler. No Mac instalas o jq com brew install jq.

O que pergunta, o que bloqueia, o que deixa passar

  • ASK, pede confirmação: rm, rmdir, unlink e find … -delete, também com sudo ou xargs à frente. Antes de procurar, o script parte o comando em ; & | ( ) e acentos graves, e assim apanha também o rm no fim de cd pasta && rm -r build.
  • NO, bloqueia e pronto: rm -r em /, ~ ou $HOME, ou seja, o disco todo ou a pasta de utilizador toda; git push com -f ou --force; git reset --hard; DROP TABLE e DROP DATABASE, em maiúsculas ou minúsculas.
  • Deixa passar: git rm --cached, docker rm e tudo o resto, porque aí o comando é git ou docker, não rm.

As duas listas, alarga-as tu. Por exemplo git clean, que apaga os ficheiros novos que o git ainda não conhece: na minha versão não está. Se o usas, põe-no em ASK ou em NO, junto com os comandos que no teu projeto fazem estragos.

O que não vê

O guardião só olha para os comandos Bash. Se o Claude esvaziar um ficheiro reescrevendo-o com a ferramenta Write, não o trava. Para os ficheiros que nunca devem mudar, como o .env, é preciso uma regra deny nas permissões, por exemplo Edit(./.env), que vale para todas as ferramentas que alteram ficheiros. Ou um segundo hook em Write.

2. Os testes: depois de cada alteração

O segundo hook usa PostToolUse, que dispara depois de uma ferramenta acabar o seu trabalho. Com o filtro «Edit|Write», arranca sempre que o Claude altera ou cria um ficheiro. Cria .claude/hooks/teste-apos-alteracao.sh:

O ficheiro
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(npm test 2>&1) && exit 0
echo "Testes falhados depois da alteração:" >&2
echo "$OUT" | tail -n 30 >&2
exit 2

Põe o teu comando de testes no lugar de npm test. Se os testes passarem, não vês nada. Se falharem, o script manda ao Claude as últimas 30 linhas do erro e sai com código 2.

Aqui o código 2 não anula a alteração: já está feita. Faz o Claude ler o erro, e assim corrige-o logo. É a diferença entre os dois hooks: o guardião pode dizer que não antes, este só pode avisar depois. E o Claude espera que os testes acabem depois de cada alteração: se os teus forem lentos, põe no script só os rápidos.

3. Ativa-os

Torna os dois scripts executáveis. No terminal, na pasta do projeto:

A linha
chmod +x .claude/hooks/*.sh

Depois, em .claude/settings.json, também na pasta do projeto, registas os dois hooks:

O ficheiro
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guardiao.sh" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/teste-apos-alteracao.sh" }] }
    ]
  }
}

Se o ficheiro já existir, não o substituas: acrescenta a parte «hooks» ao lado do que já lá está e verifica as vírgulas. $CLAUDE_PROJECT_DIR é a pasta a partir da qual abriste a sessão, assim o Claude Code encontra os scripts mesmo quando o Claude está a trabalhar numa subpasta.

Se não te apetece fazer tudo à mão, pede ao Claude:

O prompt
Cria neste projeto os dois hooks deste guia: um PreToolUse em Bash que me peça confirmação antes de apagar ficheiros e bloqueie com código 2 os comandos irreparáveis, e um PostToolUse em Edit e Write que corra [o teu comando de testes] depois de cada alteração. Regista-os em .claude/settings.json sem tirar nada do que já lá está. Depois mostra-me os dois scripts e explica-me o que faz cada linha.

4. O teste

Primeiro testo o guardião sozinho, sem o Claude. No terminal, na pasta do projeto, passo-lhe um comando a fingir, como faria o Claude Code:

A linha
echo '{"tool_input":{"command":"rm -rf teste"}}' | .claude/hooks/guardiao.sh; echo $?

Tem de aparecer um pequeno JSON com «ask» e depois 0. Com git reset --hard no lugar de rm -rf teste, tem de aparecer «Bloqueado» e depois 2. Com ls, só 0. O script só lê o texto: aqui não é executado nada. Este teste conta porque, se o caminho no settings.json estiver errado ou faltar o chmod +x, o Claude Code só mostra um aviso «hook error» e o comando passa sem perguntas.

Depois o teste a sério. Abre uma sessão nova: normalmente o Claude Code dá conta sozinho das alterações ao settings.json, mas assim tens a certeza. Escreve /hooks e confirma que estão lá os dois, um em PreToolUse e outro em PostToolUse. Depois pede ao Claude:

O prompt
Cria uma pasta vazia chamada teste-guardiao e depois apaga-a.

Tem de aparecer o pedido de permissão, com o motivo «O guardião: este comando apaga ficheiros» e a etiqueta [settings], que te diz de onde vem a pergunta. Da primeira vez responde que não e confirma que a pasta continua lá.

Para o hook dos testes:

O prompt
Muda uma linha num ficheiro do projeto para que um teste falhe, e depois diz-me o que te chegou depois da alteração. No fim, volta a pôr tudo como estava e verifica que os testes passam.

Os erros mais comuns

  • Dizer que sim por hábito: a pergunta do guardião está lá de propósito. Lê o comando antes de responder.
  • Confiar no guardião sem o ter visto trabalhar: com um caminho errado, o comando passa com um simples aviso. Faz sempre o teste à mão com echo.
  • Começar com os testes já partidos: cada alteração recebe um erro que não tem nada a ver. Antes de ativar o hook, põe os testes a passar, ou corre só os que passam.

Checklist

  • guardiao.sh em .claude/hooks, com as tuas listas ASK e NO
  • teste-apos-alteracao.sh com o teu comando de testes
  • jq instalado
  • chmod +x nos dois scripts
  • Hooks registados em .claude/settings.json
  • Teste à mão com echo: ask para rm, 2 para git reset --hard
  • Teste no Claude Code: pedido de permissão para a pasta, teste falhado reportado

Perguntas frequentes

E se de noite ninguém responder?

O comando fica parado no pedido até voltares, e nada é apagado. Vale também no modo auto. Se, pelo contrário, correres o Claude Code sem ninguém à frente, por exemplo com claude -p, o pedido não pode aparecer: o Claude Code recusa o comando e o Claude lê o motivo.

O guardião protege-me de tudo?

Não. Olha para o texto dos comandos Bash, e o mesmo estrago pode ser feito de outras formas, por exemplo com um pequeno script em Python ou reescrevendo um ficheiro com Write. Reduz o risco. A rede a sério continua a ser o git: um commit antes dos trabalhos longos e uma cópia do projeto online.

← Todos os guias