Claude Code

Correr comandos teus automaticamente com os hooks do Claude Code

Os hooks são comandos de shell que o Claude Code corre em momentos fixos, como antes de um comando ou depois de uma edição. Regista o que o Claude corre, bloqueia o que nunca deve correr, verifica cada ficheiro que edita e recebe um aviso quando está à tua espera.

Verificado na documentação oficial a

Nesta página9 secções
  1. 01O que é um hook
  2. 02Onde ficam os hooks
  3. 03Exemplo 1: registar todos os comandos
  4. 04Exemplo 2: bloquear force pushes
  5. 05Exemplo 3: verificar a sintaxe do PHP depois de cada edição
  6. 06Exemplo 4: saber quando o Claude está à tua espera
  7. 07Os hooks correm com as tuas permissões
  8. 08Quando um hook não funciona
  9. 09Fontes

Podes escrever no teu CLAUDE.md "verifica sempre a sintaxe do PHP depois de editares um ficheiro", e o Claude costuma fazê-lo. Costuma. Um hook dá a certeza. É um comando teu que o Claude Code corre sempre que chega um dado momento, decida o Claude o que decidir.

Este guia explica como funcionam os hooks e depois constrói quatro, passo a passo: um registo de todos os comandos que o Claude corre, um bloqueio aos force pushes, uma verificação de sintaxe depois de cada edição em PHP e um aviso quando o Claude está à tua espera. Os quatro foram testados em sessões reais.

O que é um hook

Um hook tem três partes:

  • Um evento: o momento em que corre. Por exemplo PreToolUse (antes de o Claude usar uma ferramenta), PostToolUse (depois de uma ferramenta correr bem) ou Notification (quando o Claude Code precisa da tua atenção).
  • Um filtro (matcher): os casos a que se aplica. Nos eventos de ferramentas, é o nome da ferramenta, como Bash ou Edit|Write.
  • Um comando: o que corre. Normalmente um comando de shell ou um script.

Quando o evento acontece, o Claude Code passa os pormenores ao teu comando em JSON, pela entrada padrão. Quando a ferramenta é o Bash, isso inclui o comando que o Claude quer correr:

JSON
{
  "session_id": "abc123",
  "cwd": "/home/tu/projeto",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

O teu comando lê o que precisa (os exemplos aqui usam o jq), faz o seu trabalho e responde com o código de saída:

  • Saída 0: nada a opor. O Claude Code continua como de costume. No PreToolUse isto não aprova nada: as regras de permissão normais continuam a aplicar-se.
  • Saída 2: um erro que bloqueia. O que escreves na saída de erros volta como motivo. Um hook PreToolUse bloqueia a chamada à ferramenta, e o Claude lê a tua mensagem. Um hook PostToolUse não consegue desfazer nada, porque a ferramenta já correu, mas o Claude lê a mensagem na mesma e pode corrigir o problema.
  • Qualquer outro código: um erro que não bloqueia. A ação avança e aparece um pequeno aviso hook error.

Os eventos que vais usar mais:

  • PreToolUse: antes de uma chamada a uma ferramenta. O único destes que consegue parar uma chamada a uma ferramenta antes de ela correr.
  • PostToolUse: depois de uma chamada a uma ferramenta correr bem.
  • Notification: quando o Claude Code está à espera da tua permissão ou de uma resposta tua.
  • SessionStart: quando uma sessão começa ou é retomada. O texto simples que o comando escrever é acrescentado ao contexto do Claude.
  • UserPromptSubmit: quando envias um pedido, antes de o Claude o ver.
  • Stop: quando o Claude acaba a resposta.

A lista completa, com mais de 30 eventos, está na referência dos hooks, nas fontes no fim.

Onde ficam os hooks

Os hooks vão para os mesmos ficheiros de definições que as regras de permissão, dentro de uma chave hooks:

  • ~/.claude/settings.json: todos os projetos da tua máquina.
  • .claude/settings.json num projeto: partilhado com toda a gente, se o puseres no git.
  • .claude/settings.local.json num projeto: só tu.

Escreve /hooks numa sessão para veres todos os hooks em vigor, agrupados por evento, e o ficheiro de onde vem cada um. O menu só os mostra. Para acrescentar ou mudar um hook, edita o ficheiro de definições, ou pede ao Claude que o faça. Normalmente o Claude Code dá pela alteração sozinho, sem reiniciar.

Cada exemplo abaixo é um ficheiro de definições completo. Se o teu já tiver uma chave hooks, junta o novo evento aos que lá estão, em vez de substituíres o bloco todo.

Exemplo 1: registar todos os comandos

Este hook acrescenta a um ficheiro do projeto cada comando de shell que o Claude corre:

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> \"$CLAUDE_PROJECT_DIR\"/.claude/command-log.txt"
          }
        ]
      }
    ]
  }
}

O Claude Code define $CLAUDE_PROJECT_DIR como a pasta do projeto, por isso o registo vai sempre parar ao mesmo sítio, esteja o Claude a trabalhar onde estiver.

Nota

O PostToolUse só corre depois de uma chamada correr bem. No nosso teste, um comando bloqueado por outro hook nunca chegou ao registo. Para registar também as chamadas que falham, acrescenta o mesmo hook em PostToolUseFailure.

Exemplo 2: bloquear force pushes

Um hook PreToolUse corre antes da verificação de permissões, por isso consegue parar um comando mesmo em modos que o deixariam correr.

  1. Escreve o script

    .claude/hooks/block-force-push.sh
    #!/bin/bash
    # Block force pushes. Claude sees the message and can choose another way.
    command=$(jq -r '.tool_input.command')
    
    if [[ "$command" =~ git[[:space:]]+push.*(--force|[[:space:]]-f([[:space:]]|$)) ]]; then
      echo "Blocked by hook: force push is not allowed in this project. Push normally or ask the user." >&2
      exit 2
    fi
    exit 0
  2. Torna-o executável

    Terminal · o teu utilizador
    chmod +x .claude/hooks/block-force-push.sh
  3. Testa-o à mão

    Dá-lhe o mesmo tipo de JSON que o Claude Code lhe mandaria e confirma o código de saída:

    Terminal · o teu utilizador
    echo '{"tool_input":{"command":"git push --force origin main"}}' | .claude/hooks/block-force-push.sh; echo "exit=$?"

    Resultado

    Blocked by hook: force push is not allowed in this project. Push normally or ask the user.
    exit=2

    Um git push origin main normal deve mostrar só exit=0.

  4. Regista-o

    .claude/settings.json
    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              {
                "type": "command",
                "if": "Bash(git push *)",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-force-push.sh"
              }
            ]
          }
        ]
      }
    }

O campo if usa os mesmos padrões que as regras de permissão. Com ele, o script só arranca para comandos git push, em vez de arrancar para todos os comandos de shell.

Quando o Claude tentou git push --force origin master no nosso teste, o comando nunca correu e o Claude recebeu:

Resultado

PreToolUse:Bash hook error: ["$CLAUDE_PROJECT_DIR"/.claude/hooks/block-force-push.sh]: Blocked by hook: force push is not allowed in this project. Push normally or ask the user.

Um hook que bloqueia funciona em todos os modos de permissão, incluindo o modo Ignorar permissões (bypassPermissions). O contrário não é verdade: um hook que responde "allow" não passa por cima de uma regra de bloqueio.

Atenção

Tal como uma regra de permissão de Bash, este script só lê o texto do comando. Um comando escrito de forma invulgar pode escapar a um padrão. Usa hooks como este para apanhar enganos. Para um limite que tem mesmo de se cumprir, junta uma regra de bloqueio à sandbox (/sandbox).

Exemplo 3: verificar a sintaxe do PHP depois de cada edição

Este hook corre php -l em cada ficheiro PHP que o Claude cria ou edita. Se a verificação falhar, o Claude lê o erro e corrige o ficheiro logo a seguir.

  1. Escreve o script

    .claude/hooks/php-lint.sh
    #!/bin/bash
    # After Claude edits a PHP file, check its syntax and report errors back to Claude.
    file=$(jq -r '.tool_input.file_path')
    
    if ! output=$(php -l "$file" 2>&1); then
      echo "PHP syntax check failed for $file:" >&2
      echo "$output" >&2
      exit 2
    fi
    exit 0
  2. Torna-o executável

    Terminal · o teu utilizador
    chmod +x .claude/hooks/php-lint.sh
  3. Regista-o para o Write e para o Edit

    .claude/settings.json
    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Edit|Write",
            "hooks": [
              {
                "type": "command",
                "if": "Write(*.php)",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/php-lint.sh"
              },
              {
                "type": "command",
                "if": "Edit(*.php)",
                "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/php-lint.sh"
              }
            ]
          }
        ]
      }
    }

Atenção

Uma condição if vale para uma só ferramenta. A nossa primeira versão tinha só Edit(*.php) e, quando o Claude criou um ficheiro novo com a ferramenta Write, a verificação nunca correu. O registo de depuração dizia Skipping hook due to if condition "Edit(*.php)" not matching. O Claude cria ficheiros novos com o Write e altera os que já existem com o Edit, por isso precisas de um de cada.

Com os dois em vigor, pedimos ao Claude que criasse um ficheiro sem um ponto e vírgula. O hook respondeu:

Resultado

PHP syntax check failed for /home/tu/projeto/hello.php:
PHP Parse error:  syntax error, unexpected end of file, expecting "," or ";" in /home/tu/projeto/hello.php on line 1
Errors parsing /home/tu/projeto/hello.php

O Claude acrescentou o ponto e vírgula com a ferramenta Edit, o hook correu outra vez sem dizer nada, e o ficheiro passou.

O mesmo esquema serve para qualquer verificador ou formatador: python -m py_compile, shellcheck, prettier --write, e por aí fora.

Exemplo 4: saber quando o Claude está à tua espera

Quando o Claude precisa da tua permissão e estás noutra janela, um aviso poupa-te a ir ver o terminal de poucos em poucos minutos. Põe este nas tuas definições de utilizador, para funcionar em todos os projetos.

Num ambiente de trabalho Linux:

~/.claude/settings.json
{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
          }
        ]
      }
    ]
  }
}

Num Mac, usa antes este comando:

Texto
osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'
  • permission_prompt corre quando um pedido de permissão já está à espera há cerca de seis segundos.
  • idle_prompt corre quando o Claude acabou de responder há cerca de um minuto e ainda não escreveste nada.
  • Um filtro vazio, "", corre em todos os tipos de aviso.

Nota

Testámos o evento num servidor, que não tem ambiente de trabalho, por isso o hook de teste escreveu num ficheiro em vez de mostrar um aviso. Correu enquanto o pedido de permissão estava à espera, e o JSON que recebeu trazia a mensagem Claude needs your permission. Os comandos notify-send e osascript são os da documentação oficial. O notify-send precisa de uma sessão gráfica, por isso não faz nada por SSH. Num Mac, o primeiro aviso pode obrigar-te a autorizar as notificações do Editor de Scripts nas Definições do Sistema.

Os hooks correm com as tuas permissões

Um hook de comando corre como tu, com acesso a tudo o que a tua conta de utilizador alcança. Antes de acrescentares um, lê o que ele faz.

Os hooks do .claude/settings.json de um projeto vêm de quem escreveu o repositório, e há duas coisas dos nossos testes que convém saber:

  • Numa sessão interativa, o Claude Code segura os hooks até confiares na pasta. Mas a caixa de confiança que vimos mostrava a regra de permissão já aprovada da pasta e não dizia nada sobre os hooks. Antes de confiares num repositório que não foste tu a escrever, abre o .claude/settings.json dele e lê tu mesmo a secção hooks.
  • O claude -p nunca mostra a caixa de confiança e trata a pasta como de confiança para os hooks. No nosso teste, ignorou a regra de autorizar do projeto porque a pasta não era de confiança, mas correu os hooks do projeto na mesma. Para correr o claude -p no repositório de outra pessoa sem os hooks dela, acrescenta --setting-sources user. Assim o Claude Code só lê as tuas definições, e os hooks e as regras do projeto ficam de fora. Testámos: com esta opção, o hook de registo do projeto não correu.

Quando um hook não funciona

  • Testa primeiro o script à mão, com uma linha de JSON, como no exemplo 2. Confirma o código de saída com echo $?.
  • Torna os scripts executáveis com chmod +x. Sem isso, o script não corre.
  • Confirma o filtro. Os filtros distinguem maiúsculas de minúsculas: bash não é Bash.
  • jq: command not found: instala o jq, ou escreve o hook em Python.
  • Vê o que aconteceu. Carrega em Ctrl+O para a vista da transcrição, onde aparecem as mensagens de bloqueio e os erros dos hooks. Um hook que corre bem não mostra ali nada.
  • Lê o registo de depuração. Arranca o Claude Code com claude --debug-file /tmp/claude.log, e o registo mostra que hooks corresponderam, quais foram saltados e porquê, e o que cada um escreveu.

Fontes

Verificado em

Claude Code 2.1.280 em AlmaLinux 9.8, com jq 1.6 e PHP 8. Os quatro hooks foram testados num projeto git descartável: os exemplos 1 a 3 em execuções com claude -p, e o exemplo 4 e a caixa de confiança numa sessão interativa. As mensagens acima foram copiadas dessas sessões, com o caminho do projeto trocado por /home/tu/projeto.