Blogue

Hooks no Claude Code: configuração e 7 exemplos em 2026

Markdown

7 hooks prontos a usar para .claude/settings.json, prontos a copiar e colar: proteção de .env, prettier, force-push para main, travessões e um servidor de desenvolvimento. 4 tipos de handlers e 31 eventos.

Ler o artigo completo

Os hooks no Claude Code são comandos de shell, chamadas HTTP ou verificações curtas com LLM que são executados automaticamente antes ou depois de uma ação do Claude. Acrescentam verificações a eventos concretos, formatam código após editar e injetam contexto no arranque. Um PreToolUse correspondente pode rejeitar essa chamada; os exemplos de expressões regulares são proteções práticas, não um limite de segurança para todo o acesso a ficheiros ou Git. Segue-se uma explicação dos 4 tipos de handlers, 31 eventos do ciclo de vida, um teste para determinar “quando uma regra deve ser um hook”, 7 hooks prontos a copiar para .claude/settings.json e 3 armadilhas com que me deparei.

Documentação da Ailoot: API, chaves e configuração.

O que são hooks no Claude Code?

Principal conclusão

Um hook é um comando de shell, uma chamada HTTP, um prompt curto para LLM ou um subagente que o Claude Code executa em resposta a um evento específico do ciclo de vida. Ao contrário das instruções em CLAUDE.md, que o modelo pode ignorar ou esquecer, um hook é executado sempre da mesma forma: é código, não uma sugestão.

Durante meio ano, mantive a regra “nunca edites .env diretamente” em CLAUDE.md. O Claude violava-a uma vez por mês. Não de propósito; simplesmente esquecia-se dela no final de uma sessão longa. Uma vez, foi parar um token extra ao .env; noutra ocasião, uma variável com um nome duplicado substituiu a que estava a ser usada.

Quando uma ferramenta de edição direta correspondente fornece um caminho .env, o hook configurado rejeita a chamada. Redirecionamentos Bash, scripts, ferramentas MCP e outros percursos precisam de controlos separados; o exemplo não torna o ficheiro imutável.

Os hooks são comandos de shell, endpoints HTTP ou prompts de LLM que defines e que são executados automaticamente em momentos específicos do ciclo de vida do Claude Code.

— Anthropic, https://code.claude.com/docs/en/hooks

Por outras palavras, um hook é um comando teu que o Claude Code executa num evento específico. Cada edição de ficheiro, execução de Bash, início de sessão ou conclusão de subagente é um evento ao qual podes associar um hook.

Porque é que isto é importante para quem programa com vibe coding:

  • Protege ficheiros que contêm segredos contra edições acidentais.
  • Executa automaticamente prettier/eslint após cada geração de código.
  • Bloqueia git push --force para main sem autorização explícita.
  • Injeta contexto atualizado no início de cada sessão.
  • Regista as ações do Claude para auditoria.

O CLAUDE.md trata do contexto. Os hooks tratam das ações. São duas coisas diferentes, e confundi-las é o principal erro de principiante.

Em que diferem os hooks do CLAUDE.md e das skills?

Principal conclusão

CLAUDE.md é texto que o modelo lê no início de uma sessão e tenta seguir. Um hook é um comando de shell que o processo do Claude Code executa em resposta a um evento, independentemente do modelo. Uma skill é um conjunto de instruções com um nome que o Claude pode invocar com a ferramenta Skill, como uma função. Os três servem tipos de tarefas diferentes.

A documentação da Anthropic explica explicitamente a diferença entre CLAUDE.md e os hooks:

O conteúdo de CLAUDE.md é enviado como uma mensagem do utilizador depois do prompt de sistema, não como parte do prompt de sistema. O Claude lê-o e tenta segui-lo, mas não há garantia de cumprimento rigoroso.

— Anthropic, documentação sobre memória, https://code.claude.com/docs/en/memory

Por outras palavras, o conteúdo de CLAUDE.md chega ao Claude como a primeira mensagem do utilizador, não como uma instrução de sistema. O modelo lê-o e tenta segui-lo. Mas não há garantias, sobretudo no final de uma sessão longa, quando o contexto aumenta e as instruções mais antigas são expulsas.

Um hook funciona de outra forma. Está em settings.json, não depende do modelo e é executado pelo processo do Claude Code num evento específico: PreToolUse, PostToolUse, SessionStart, entre outros. O comando é executado antes de o Claude receber o resultado e pode bloquear fisicamente uma ação (exit 2) ou modificar um ficheiro depois da edição (exit 0).

Comparação:

FuncionalidadeCLAUDE.mdHookSkill
Onde se encontraCLAUDE.md na raiz do projeto.claude/settings.jsonPasta .claude/skills/
Quem o executaO modelo lê-o como parte de uma mensagem do utilizadorO processo do Claude Code perante um eventoO modelo através da ferramenta Skill
Garantia de execuçãoNão (melhor esforço)Um hook de comando configurado correspondente executa código; falhas e códigos de saída dependem do eventoAtravés de uma chamada ao modelo (com autorização, mas não ao nível do shell)
Para que serveContexto, tom, estilo e encaminhamento do projetoRestrições, formatação e verificações de ficheiros específicosProcedimentos complexos com várias etapas e parâmetros
Quando é executadoEm cada mensagem do modelo numa sessãoNum evento do ciclo de vidaQuando o modelo o invoca
Pode bloquear uma ação?NãoSim (exit 2)Não

O modelo completo de CLAUDE.md e 6 regras para manter um ficheiro vivo estão no guia Como configurar o CLAUDE.md em 2026: um modelo pronto a usar e 6 regras. Aprende sobre skills como capacidades com nome em Claude Code Skills: como criar a tua própria biblioteca.

Um hook é o nível mais baixo: sem modelo, sem interpretação, apenas shell.

Os hooks são uma das ferramentas de engenharia de contexto, a par do Plan Mode e do /compact.

4 tipos de hooks: Command, HTTP, Prompt, Agent — qual deves escolher?

Principal conclusão

O Claude Code tem 4 tipos de handlers. Command é um script de shell e o tipo mais comum. HTTP envia um pedido POST para o teu URL, para equipas e integrações. Prompt faz uma chamada curta ao Haiku para uma verificação semântica. Agent cria um subagente com acesso a ferramentas para validação aprofundada. Para começar, basta usar Command e Prompt; cobrem 90% das tarefas.

TipoO que fazQuando escolherComplexidade
CommandExecuta um comando de shell, recebe JSON em stdin e responde com um código de saída e stdout90% dos casos: verificar caminhos, formatar, usar grep no conteúdo, lsof, operações gitbaixa
HTTPEnvia um pedido POST para o teu URL com um corpo JSON e recebe JSON na respostaQuando uma regra pertence à equipa/CI: validação centralizada, painel de estado partilhado, auditoria remotamédia (requer um servidor)
PromptEnvia um prompt curto ao Haiku (por predefinição) e recebe uma resposta sim/não para uma avaliação semânticaQuando o grep não chega: “avalia se este texto soa empresarial”baixa
AgentCria um subagente com acesso Read/Grep/Glob para verificações com várias etapasValidação exigente: “encontra todos os ficheiros alterados e verifica se existem testes correspondentes”alta

O erro mais comum é tentar fazer com Command algo que exige Prompt. Por exemplo, “não uses travessões em ficheiros .md” é uma tarefa para grep, por isso Command é ideal. Mas “não escrevas num tom empresarial” é uma questão semântica e precisa de Prompt.

Se estás a começar, usa Command. Os 7 hooks prontos a usar abaixo são todos Command. HTTP/Prompt/Agent vêm depois, quando estiveres preparado.

Onde está o settings.json e quais são os 4 níveis de configuração?

Principal conclusão

O Claude Code procura configuração em 4 locais: enterprise (implementada através de MDM), utilizador (~/.claude/settings.json), projeto (.claude/settings.json na raiz do repositório) e local (.claude/settings.local.json, não incluído no git). Os 4 ficheiros são combinados no início da sessão, com prevalência do nível inferior.

NívelCaminhoQuando se aplicaNo git?
Enterprise/Library/Application Support/ClaudeCode/settings.json (Mac), /etc/claude-code/settings.json (Linux)Em toda a equipa através de MDMnão
Utilizador~/.claude/settings.jsonTodos os projetos de um utilizadornão
Projeto.claude/settings.json na raiz do repositórioEste projeto, para toda a equipasim
Local.claude/settings.local.jsonApenas este projeto e este utilizadornão (em .gitignore)

Mantenho os 7 hooks descritos abaixo no .claude/settings.json do projeto, para que sejam incluídos automaticamente quando o repositório é clonado e o Claude siga as mesmas regras numa nova janela. Os hooks experimentais pessoais ficam em .claude/settings.local.json para não interferirem com a equipa.

Os 4 ficheiros são combinados no início da sessão. Se houver regras em conflito, prevalece o nível inferior (local sobre projeto, projeto sobre utilizador).

A forma mais rápida de criar o primeiro hook é usar o menu interativo /hooks dentro do Claude Code. Escolhe um evento da lista, seleciona um matcher (por exemplo, Bash ou Edit) e cola um comando. O Claude Code escreve JSON válido no settings.json por ti.

31 eventos do ciclo de vida: de quais precisam os principiantes?

Principal conclusão

Atualmente, o Claude Code tem 31 eventos do ciclo de vida, divididos por 3 frequências: uma vez por sessão (SessionStart/SessionEnd), uma vez por turno (UserPromptSubmit/Stop) e em cada chamada de ferramenta (PreToolUse/PostToolUse). Os principiantes precisam de 4 dos 31 eventos: PreToolUse para restrições, PostToolUse para edições automáticas, SessionStart para contexto e Stop para verificações finais.

Os eventos ocorrem em três frequências: uma vez por sessão (SessionStart, SessionEnd), uma vez por turno (UserPromptSubmit, Stop, StopFailure) e em cada chamada de ferramenta dentro do ciclo agentic (PreToolUse, PostToolUse).

— Anthropic, referência de hooks, https://code.claude.com/docs/en/hooks

É importante compreender as três frequências para não te confundires:

  1. Uma vez por sessão - SessionStart, SessionEnd, Setup. São executados uma vez quando o Claude Code abre e fecha. Usa-os para inicializar o contexto e limpar ficheiros temporários.
  2. Uma vez por turno - UserPromptSubmit, UserPromptExpansion, Stop, StopFailure. São acionados em cada uma das tuas mensagens e sempre que o modelo para. Usa-os para registar a conversa e fazer verificações finais do resultado.
  3. Em cada chamada de ferramenta - PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, PostToolBatch, SubagentStart, SubagentStop. São acionados em cada edição de ficheiro, execução de Bash, leitura ou pesquisa. É a camada mais ativa, onde se aplicam restrições, formatação e validações.

A lista completa dos 31 eventos inclui eventos assíncronos/reativos: TeammateIdle, PreCompact, PostCompact, Elicitation, ElicitationResult, InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, Notification. São para cenários avançados; por agora, ignora-os.

Teste de cinco perguntas: quando deve uma regra ser um hook e quando deve ficar no CLAUDE.md?

Principal conclusão

Se uma regra diz respeito a uma ação (não ao contexto), envolve segurança/dinheiro/dados, pode ser verificada com grep/regex, é difícil reparar os danos se for violada e o Claude já a violou pelo menos uma vez, deve ser um hook. Se menos de três das cinco respostas forem “sim”, fica no CLAUDE.md. Apliquei o teste às minhas 80 linhas e passei 5 regras para hooks.

Para mim, isto funciona como uma lei, não como uma sugestão. Reduzi a escolha entre CLAUDE.md e um hook a um teste simples com cinco perguntas. Se pelo menos três respostas forem “sim”, a regra passa para um hook. Com menos de três, fica no texto.

O teste “quando deve uma regra ser um hook?”:

  1. Esta regra diz respeito a uma ação, e não ao contexto? (“não executes rm -rf” é uma ação; “tenho um projeto TypeScript” é contexto.)
  2. Esta regra diz respeito à segurança, ao dinheiro ou aos dados? (“não toques no .env”, “não publiques chaves”.)
  3. É possível verificá-la com grep, regex ou o código de saída de um comando de shell? (“depois de editar .md, verifica se não há travessões”.)
  4. Se a regra for violada, será doloroso reparar as consequências? (“force-push para main” é difícil de reparar; “esquecer um ponto final” é fácil.)
  5. Já viste o Claude esquecer-se desta regra pelo menos uma vez?

Exemplo: “Não uses travessões; usa apenas hífenes.” Ação? Sim. Segurança? Não. É verificável com grep? Sim. É difícil de reparar? Não. Já viste a regra ser violada? Sim. 3 em 5: coloca-a num hook.

Contraexemplo: “Trabalho com vibe coders, não programadores; escreve sem calão.” Ação? É mais contexto. Segurança? Não. Verificável com regex? Demasiado difícil. É difícil de reparar? Não. Já viste a regra ser violada? Sim. 1 em 5: mantém-na no CLAUDE.md.

Cinco das minhas regras passaram o teste. Passaram para hooks. Os restantes 75% ficaram no ficheiro de texto.

7 hooks prontos a copiar

Principal conclusão

Abaixo estão 7 hooks prontos a usar para .claude/settings.json: bloquear edições de .env, executar prettier automaticamente, impedir force-push para main, detetar travessões, libertar uma porta antes de npm run dev, fazer commit automaticamente após testes bem-sucedidos e injetar contexto atualizado no início de uma sessão. Copia cada um para o ficheiro, reinicia o Claude Code e fica a funcionar.

Os 7 hooks estão guardados num único ficheiro .claude/settings.json na raiz do projeto. Cada entrada tem um tipo de evento, um filtro de ferramenta (matcher) e um comando de shell. O código 2 rejeita uma chamada PreToolUse correspondente antes de executar. Em PostToolUse, a ação já ocorreu: comunica feedback e não a desfaz.

Na maioria dos eventos de hooks, só o código de saída 2 bloqueia uma ação. O Claude Code trata o código de saída 1 como um erro não bloqueante e continua a ação, apesar de 1 significar convencionalmente falha em Unix.

— Anthropic, referência de hooks, https://code.claude.com/docs/en/hooks

exit 0 significa sucesso. exit 1 significa erro, mas o Claude continua (este é o primeiro antipadrão abaixo). exit 2 depende do evento: pode bloquear PreToolUse, enquanto PostToolUse só comunica feedback sobre uma ação já realizada.

Hook 1. Proteger edições diretas de .env (PreToolUse, exit 2)

Bloqueia tentativas de editar diretamente qualquer ficheiro que corresponda a .env, .env.local, .env.production e assim por diante.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '(^|/)\\.env(\\.|$)' && { echo 'Do not edit `.env` directly. Tell me which variable to add, and I’ll open the file myself.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

O que faz: Antes de cada Edit/Write/MultiEdit, obtém o caminho do ficheiro, verifica com grep se corresponde ao padrão .env e, se corresponder, devolve exit 2 com uma mensagem. O Claude vê stderr e diz-me: “Tentei editar .env, mas o hook bloqueou-me; diz-me que variável devo adicionar.”

Hook 2. Executa prettier automaticamente depois de cada edição de código (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(ts|tsx|js|jsx|json)$' && npx prettier --write \"$FILE\" 2>/dev/null & exit 0"
          }
        ]
      }
    ]
  }
}

O que faz: Depois de cada Edit/Write, obtém o caminho e, se a extensão for .ts/.tsx/.js/.jsx/.json, executa prettier em segundo plano com &. Termina sempre com exit 0 porque se trata de formatação, não de uma restrição. Se o prettier falhar, não há problema; o ficheiro fica como estava.

A diferença em relação ao hook 1 é fundamental: PreToolUse + exit 2 é um bloqueio rígido antes da ação. PostToolUse + exit 0 é uma edição suave depois da ação. São dois modos diferentes para tarefas diferentes.

Hook 3. Bloqueia git push --force para o ramo main (PreToolUse, exit 2)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE 'git[[:space:]]+push' && echo \"$CMD\" | grep -qE '(\\-\\-force|\\-\\-force-with-lease|[[:space:]]-f([[:space:]]|$))' && echo \"$CMD\" | grep -qE '([[:space:]/:])main([[:space:]/:]|$)' && { echo 'Force push to main is forbidden. Create a new branch or ask me to do it manually.' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

O que faz: Em cada execução de Bash, verifica se o comando inclui push, inclui --force/--force-with-lease/-f e menciona main. Se as três condições forem verdadeiras, bloqueia-o com uma mensagem clara.

Antes, tinha esta regra em maiúsculas no CLAUDE.md. O Claude continuava a violá-la cerca de uma vez a cada duas semanas quando lhe pedia para “fazer force-push para desfazer o meu commit errado”. Este hook deteta as formas literais indicadas, mas aliases, scripts, branch de destino implícito ou outro cliente Git podem escapar. Imponha proteção de main no remoto e limite as credenciais do agente.

Hook 4. Bloqueia travessões em ficheiros de texto (PostToolUse, exit 2)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; echo \"$FILE\" | grep -qE '\\.(md|mdx|tsx?)$' && grep -q $'\\xe2\\x80\\x94' \"$FILE\" && { echo \"File $FILE contains an em dash (U+2014). Replace it with a regular hyphen (U+002D).\" >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

O que faz: PostToolUse com exit 2 comunica ao Claude uma falha posterior à edição. Não reverte nem impede a escrita.

Repara que o grep procura especificamente o carácter de travessão (U+2014) através de $'\xe2\x80\x94', a notação Bash para o escrever como bytes UTF-8. Se escreveres simplesmente grep -q '-', vai detetar hífenes normais e executar-se em todos os ficheiros. Um carácter invisível estraga toda a lógica.

Hook 5. Liberta a porta 3000 antes de npm run dev (PreToolUse, exit 0)

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; echo \"$CMD\" | grep -qE '(npm|pnpm|yarn)[[:space:]]+run[[:space:]]+dev' && { lsof -ti:3000 | xargs -r kill -9 2>/dev/null; echo 'Port 3000 freed before launch' >&2; }; exit 0"
          }
        ]
      }
    ]
  }
}

O que faz: Antes de cada npm run dev (ou pnpm/yarn), termina o processo na porta 3000. Termina sempre com exit 0 porque se trata de preparação, não de uma restrição. Evita uma classe de armadilhas em que o Claude vê “port in use”, pensa que existe um problema no código e começa a corrigir coisas, quando na realidade o servidor antigo simplesmente ficou a correr.

Hook 6. Faz commit automaticamente após testes bem-sucedidos (PostToolUse, exit 0)

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "INPUT=$(cat); CMD=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty') || exit 1; EXIT=$(printf '%s' \"$INPUT\" | jq -r '.tool_response.exit_code // empty') || exit 1; case \"$CMD\" in 'npm test'|'pnpm test'|'yarn test') ;; *) exit 0 ;; esac; [ \"$EXIT\" = \"0\" ] || exit 0; git diff --cached --quiet || { echo 'Existing staged changes: review manually' >&2; exit 0; }; git add -- src/orders.ts tests/orders.test.ts || exit 1; git diff --cached --quiet && exit 0; git commit -m 'tests: checkpoint after successful run'"
          }
        ]
      }
    ]
  }
}

O que faz: Lê stdin JSON uma vez e aceita só os comandos de teste simples exatos indicados. Continua só se tool_response.exit_code for explicitamente zero; se a versão não fornecer o campo, omite o commit em vez de adivinhar sucesso. Substitui src/orders.ts e tests/orders.test.ts pelos ficheiros exatos da tarefa. Alterações já preparadas param a receita para evitar incluir trabalho alheio. Falha em add interrompe, diff vazio não cria commit e hooks normais mantêm-se. Testa offline entradas de sucesso, falha e resultado ausente antes de ativar.

Antipadrão: acrescentar git commit --no-verify a cada Edit. Mantém verificações de commit ativas. Se uma verificação causar recursão, restringe o acionador ou redesenha a verificação; não contornes por predefinição. Revê diff preparado e titularidade da tarefa antes de commits automáticos.

Hook 7. Injeta contexto atualizado no início da sessão (SessionStart com additionalContext)

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo 'no-branch'); STATUS=$(git status --short 2>/dev/null | head -10); echo \"{\\\"additionalContext\\\":\\\"Current branch: $BRANCH. Uncommitted changes:\\n$STATUS\\\"}\""
          }
        ]
      }
    ]
  }
}

O que faz: No início da sessão, recolhe o ramo Git atual e a lista de alterações por confirmar, depois devolve JSON com um campo additionalContext. O Claude Code injeta este texto na primeira mensagem de sistema, para que o Claude saiba desde o início onde está e o que ficou da sessão anterior.

Usa additionalContext para informações que o Claude deve conhecer sobre o estado atual do ambiente ou uma operação acabada de concluir: estado do ambiente (ramo atual, destino de implementação, sinalizadores de funcionalidades ativos), regras condicionais do projeto (qual o comando de teste aplicável ao ficheiro acabado de editar), dados externos (tarefas abertas atribuídas a ti, resultados recentes de CI).

— Anthropic, referência de hooks, https://code.claude.com/docs/en/hooks

Extensões deste hook: obter o commit mais recente e a respetiva descrição, o estado do GitHub Actions, o sprint ativo no Linear/Jira e as tarefas abertas do repositório atual.

3 armadilhas dos hooks que apanham uma pessoa em cada duas

Principal conclusão

A primeira armadilha: um hook com exit 1 passa silenciosamente e o Claude ignora o erro. A segunda: trabalho pesado num hook é terminado após um timeout de 60 segundos. A terceira: um hook só vê as variáveis de ambiente de ~/.zshrc, não o .env do projeto. Cada uma se resolve com uma linha, mas, sem conhecer estas armadilhas, pode parecer que “os hooks não funcionam”.

Armadilha 1. Um hook com exit 1 passa silenciosamente

A mais perigosa. Durante uma semana, pensei que o meu protetor de .env estava a funcionar, mas afinal tinha jq -r '.tool_input.file_path' no hook sem verificar se o valor era null. Quando tool_input estava vazio, o jq terminava com o código 1. O Claude tratava exit 1 como “erro no hook, continuar”, pelo que a ação avançava. Não percebia porque continuavam a mexer no .env. Só vi a causa quando executei o hook manualmente num terminal com entrada vazia.

A solução resume-se a uma regra: começa cada hook por verificar se o valor é null e termina-o com || exit 0. Qualquer saída inesperada passa assim a resultar numa saída “segura”, em vez de ser ignorada silenciosamente.

# Safe start and end template:
FILE=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
# ... your logic ...
exit 0   # or || exit 0 on one line

Armadilha 2. Os hooks lentos são terminados por um timeout

Tinha um hook que executava eslint --fix no ficheiro inteiro depois de Edit. Os ficheiros grandes (1000+ linhas) demoravam 8–10 segundos a processar. O timeout predefinido dos hooks do Claude Code é de 60 segundos, por isso pensei que estava dentro do limite. Mas, quando ocorrem várias edições num minuto, os hooks acumulam-se e ficam em fila, e alguns são simplesmente ignorados. Só percebi quando o registo mostrou “hook timed out”.

Solução: se um hook exigir trabalho pesado, executa-o em segundo plano. eslint --fix "$FILE" & sem esperar pelo resultado. O hook termina então com exit 0 em milissegundos, enquanto o trabalho propriamente dito decorre em segundo plano.

# Heavy operation in background:
npx eslint --fix "$FILE" &
exit 0

Armadilha 3. Um hook não consegue ver as variáveis de ambiente do projeto

Pensei que process.env estaria disponível num hook tal como numa aplicação Node.js. Não está. O hook é executado num shell iniciado pelo processo principal do Claude Code e só vê o que está em ~/.zshrc ou ~/.bashrc, além do que o Claude Code lhe passa explicitamente. Se o teu hook depender de OPENAI_API_KEY de .env, terás de ler .env manualmente.

Solução: lê explicitamente .env no comando do hook.

# Passing project env into hook:
cd "$PWD" && source .env && curl -H "Authorization: Bearer $OPENAI_API_KEY" ...
# Or with dotenv:
dotenv -e .env -- node my-validator.js

Isto é especialmente problemático num monorepo, onde diferentes pacotes têm os seus próprios ficheiros .env. Resolve-o passando PWD e executando explicitamente cd "$PWD" no início do comando.

Como testar os hooks antes de fazer commit?

Principal conclusão

Um hook é um comando de shell. Podes executá-lo manualmente num terminal com stdin fictício, verificar o código de saída e stderr, e fazer tudo isso antes de o Claude Code o executar. Isto poupa horas de depuração e protege-te contra hooks que bloqueiam a sua própria operação.

Cenário de teste básico. Copia o comando do hook do JSON, abre um terminal e envia-lhe o JSON que o Claude Code enviaria:

# Test hook 1 (protect .env):
echo '{"tool_input":{"file_path":".env"}}' | bash -c '
FILE=$(jq -r ".tool_input.file_path // empty")
[ -z "$FILE" ] && exit 0
echo "$FILE" | grep -qE "(^|/)\.env(\.|$)" && {
  echo "BLOCKED" >&2
  exit 2
} || exit 0
'
echo "Exit code: $?"

Deves ver BLOCKED em stderr e Exit code: 2. Se obtiveres exit 0, a regex não está a funcionar; corrige o grep.

Cenários a verificar:

  1. Caminho esperado - o ficheiro deve ser bloqueado. .env para o hook 1, git push --force main para o hook 3.
  2. Caminho negativo - o ficheiro não deve ser bloqueado. README.md para o hook 1, git push origin feature-branch para o hook 3.
  3. Casos limite - entrada vazia, valores null, caracteres inesperados. O hook deve devolver exit 0 ou falhar com um erro claro, não passar silenciosamente.

Onde colocar hooks experimentais. Não em .claude/settings.json, mas em .claude/settings.local.json (não incluído no git). Coloca aí os hooks novos em PostToolUse com echo em vez da ação real, verifica se são acionados e depois passa-os para o settings.json de produção.

// Experiment in settings.local.json - writes to /tmp/hook-debug.log:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo \"[hook fired] $(date) $(jq -r '.tool_input.file_path // \"?\"')\" >> /tmp/hook-debug.log"
          }
        ]
      }
    ]
  }
}

Depois de 5–10 edições no Claude Code, abre /tmp/hook-debug.log para veres que ficheiros acionaram o hook. Se estiver vazio, o matcher não corresponde; corrige a regex.

E agora?

Principal conclusão

Começa com hooks PostToolUse que terminem com exit 0; são os mais seguros e não bloqueiam nada. Depois de uma semana, adiciona o teu primeiro hook PreToolUse com exit 2 para proteger .env ou impedir force-push. Ao fim de duas semanas, o CLAUDE.md passará de um contrato de 200 linhas para uma descrição de contexto curta, com 50–80 linhas, com certas verificações de eventos em hooks e limites rígidos de acesso impostos separadamente por permissões, sandbox e proteções remotas.

5 passos para a próxima semana:

  1. Procura no teu CLAUDE.md palavras como “nunca”, “proibido”, “sempre” e “tem de”. Cada regra deste tipo é candidata a hook. Se uma regra for imperativa, o ficheiro de texto não a impõe.
  2. Começa com PostToolUse + exit 0. São as opções mais seguras: fazem uma alteração, mas não bloqueiam nada. Prettier (hook 2), terminar o servidor de desenvolvimento (hook 5), commit automático após testes (hook 6).
  3. Só quando tiveres confiança, adiciona o teu primeiro PreToolUse + exit 2. Por exemplo, protege .env (hook 1) ou impede force-push (hook 3). É um bloqueio rígido, e um erro no comando grep pode bloquear-te a ti próprio.
  4. Testa os hooks manualmente antes de fazer commit (consulta a secção acima). Poupa horas de depuração.
  5. Usa settings.local.json para experiências. Coloca aí hooks novos com echo em vez da ação real.

Depois de configurares os hooks, o próximo passo desta série é como anular alterações no Claude Code, caso um hook deixe passar algo perigoso. Se quiseres que o Claude se lembre da estrutura do teu projeto entre sessões, cria um Segundo Cérebro no Claude Code (pasta business/ + base de conhecimento ligada). E para dar nomes às skills do teu projeto para que possam ser reutilizadas, consulta Claude Code Skills.

Se precisares de acesso a redes neuronais através da Ailoot, espreita a Ailoot.

Mais detalhes

Perguntas frequentes

Porque deve um hook ler a entrada JSON só uma vez?

Stdin é um fluxo: o primeiro jq consome-o e as leituras seguintes chegam ao fim. Guarde a entrada uma vez e extraia daí comando e resultado. Trate resultado ausente ou desconhecido como não verificado e omita o commit automático.

Os hooks de expressão regular criam um limite de segurança?

Não. Verificam eventos e comandos reconhecíveis. Redirecionamentos, aliases, scripts, MCP ou outras escritas podem escapar; use credenciais restritas, permissões, sandbox e branches protegidos para limites rígidos.

Todos os artigos