Segurança com hooks — defesa em profundidade

TL;DR

Segurança com hooks vai além de guardrails individuais: é uma estratégia em camadas que cobre o fluxo de trabalho inteiro — desde bloquear comandos destrutivos até proteger arquivos sensíveis, controlar o que pode ser commitado, e detectar credenciais antes de ir ao git. Guardrail bloqueia uma ação. Segurança sistêmica cobre o ciclo completo: o que pode executar, o que pode editar, o que pode commitar, o que pode publicar.


Vídeo — Setting up Claude Code security guardrails

NextWork, 2026 demonstra na prática as três camadas de defesa — permission deny-rules, hooks PreToolUse que inspecionam o tool_input e bloqueiam via exit code 2, e políticas em CLAUDE.md como camada comportamental mais leve — incluindo um exercício de red-team simulando .env falso, cat .env e echo $(cat .env) pra verificar qual camada pega qual ataque. Complementa bem as cinco camadas desta nota.


A analogia: a auditoria de acesso em um ambiente regulado

Em ambientes regulados (bancos, hospitais, sistemas de saúde), não basta bloquear ações perigosas na porta. A segurança é sistêmica: cada ação é registrada, o que pode ser lido está definido, o que pode ser modificado requer aprovação, e o histórico é imutável para auditoria. Não é desconfiança — é accountability.

Hooks bem configurados fazem o mesmo para o Claude Code. Não se trata de desconfiar do agente — se trata de ter um registro confiável de o que aconteceu, garantir que arquivos críticos nunca foram tocados, e que o histórico git permanece intacto. É a diferença entre “usamos IA com auto mode” e “usamos IA com auto mode em ambiente auditável”.


A diferença entre guardrail e segurança sistêmica

Um guardrail bloqueia um comando. Uma estratégia de segurança com hooks cobre o ciclo completo:

flowchart LR
    A["O que pode\nexecutar?"] --> B["PreToolUse\nBash"]
    C["O que pode\neditar?"] --> D["PreToolUse\nEdit/Write"]
    E["O que pode\ncommitar?"] --> F["PreToolUse\ngit commit"]
    G["O que pode\nser publicado?"] --> H["PreToolUse\ngit push"]
    I["Credenciais\nno código?"] --> J["PostToolUse\ngit add"]
    K["Histórico\nde auditoria"] --> L["PreToolUse\nmatcher vazio"]

    style A fill:#2980b9,color:#fff
    style C fill:#2980b9,color:#fff
    style E fill:#2980b9,color:#fff
    style G fill:#2980b9,color:#fff
    style I fill:#2980b9,color:#fff
    style K fill:#2980b9,color:#fff

Cinco camadas, cinco hooks distintos. Cada um protege um ponto diferente do fluxo.


Camada 1 — Proteção de arquivos sensíveis

A primeira linha: impedir que o agente leia ou edite arquivos que nunca devem ser tocados.

#!/bin/bash
# ~/.claude/hooks/protect-files.sh
 
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
 
# Bloquear Edit, Write E Read em arquivos sensíveis
if [[ "$TOOL" != "Edit" && "$TOOL" != "Write" && "$TOOL" != "Read" ]]; then exit 0; fi
 
BLOCKED_PATTERNS=(
  ".*\.env$"
  ".*\.env\."
  ".*\.pem$"
  ".*\.key$"
  ".*\.pfx$"
  ".*credentials\.json$"
  ".*secret(s)?\.(json|yaml|yml)$"
  ".*/\.ssh/.*"
  ".*id_rsa$"
  ".*id_ed25519$"
)
 
for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if echo "$FILE" | grep -qiE "$pattern"; then
    echo "SEGURANÇA: $FILE é um arquivo sensível." >&2
    echo "Leitura e edição pelo agente bloqueadas. Acesse manualmente." >&2
    exit 1
  fi
done
 
exit 0

Por que bloquear Read também

Um agente que lê suas chaves privadas pode inadvertidamente incluí-las em output, logs, ou como conteúdo de uma chamada API subsequente. Bloquear Read é conservative — se o agente precisa de alguma credencial, forneça via variável de ambiente, não via arquivo.


Camada 2 — Detecção de credenciais antes de commit

Interceptar git add e git commit para verificar se o staged content tem credenciais:

#!/bin/bash
# hooks/detect-credentials-on-commit.sh
 
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
 
# Intercepta git add e git commit (PreToolUse)
if ! echo "$COMMAND" | grep -qE "^git (add|commit)"; then exit 0; fi
 
# Verifica o staged content atual
STAGED_CONTENT=$(git diff --staged 2>/dev/null)
 
PATTERNS=(
  "AKIA[0-9A-Z]{16}"                          # AWS Access Key ID
  "sk-[a-zA-Z0-9]{48}"                         # OpenAI API key
  "ghp_[a-zA-Z0-9]{36}"                        # GitHub PAT
  "ya29\.[a-zA-Z0-9_-]+"                       # Google OAuth token
  "password\s*[:=]\s*['\"][^'\"]{6,}['\"]"     # password = "valor"
  "secret\s*[:=]\s*['\"][^'\"]{6,}['\"]"       # secret = "valor"
  "api.?key\s*[:=]\s*['\"][^'\"]{6,}['\"]"     # api_key = "valor"
  "token\s*[:=]\s*['\"][^'\"]{10,}['\"]"       # token = "valor longo"
)
 
for pattern in "${PATTERNS[@]}"; do
  if echo "$STAGED_CONTENT" | grep -qE "$pattern"; then
    echo "SEGURANÇA: possível credencial detectada no staged content." >&2
    echo "Padrão detectado: $pattern" >&2
    echo "Revise com: git diff --staged | grep -E '$pattern'" >&2
    exit 1
  fi
done
 
exit 0

Configure como PreToolUse (não PostToolUse) — assim bloqueia antes de o commit acontecer:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/detect-credentials-on-commit.sh" }]
      }
    ]
  }
}

Camada 3 — Proteção do histórico git

Bloquear operações que reescrevem histórico e podem causar perda de trabalho dos colegas:

#!/bin/bash
# ~/.claude/hooks/protect-git-history.sh
 
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
 
echo "$COMMAND" | grep -q "^git " || exit 0
 
# Force push — reescreve histórico remoto
if echo "$COMMAND" | grep -qE "push.*(--force\b|-f\b)"; then
  echo "SEGURANÇA: force push bloqueado — pode apagar commits de outros." >&2
  echo "Use --force-with-lease para verificar que não houve push externo." >&2
  exit 1
fi
 
# Rebase em branches compartilhadas — reescreve histórico local e depois force push
BRANCH=$(git branch --show-current 2>/dev/null || echo "")
if echo "$COMMAND" | grep -qE "^git rebase" && [[ "$BRANCH" =~ ^(main|master|develop)$ ]]; then
  echo "SEGURANÇA: git rebase em branch compartilhada ($BRANCH) bloqueado." >&2
  echo "Rebase é seguro apenas em feature branches pessoais." >&2
  exit 1
fi
 
# Reset --hard — descarta trabalho não commitado permanentemente
if echo "$COMMAND" | grep -qE "reset\s+--hard"; then
  echo "SEGURANÇA: git reset --hard bloqueado." >&2
  echo "Use git stash para preservar o trabalho antes de resetar." >&2
  exit 1
fi
 
# Amend de commits já publicados
if echo "$COMMAND" | grep -qE "commit.*--amend"; then
  REMOTE_TRACKING=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)
  if [[ -n "$REMOTE_TRACKING" ]]; then
    LOCAL=$(git rev-parse HEAD 2>/dev/null)
    REMOTE_HASH=$(git rev-parse "$REMOTE_TRACKING" 2>/dev/null)
    if [[ "$LOCAL" == "$REMOTE_HASH" ]]; then
      echo "SEGURANÇA: --amend em commit já publicado no remote bloqueado." >&2
      exit 1
    fi
  fi
fi
 
exit 0

Camada 4 — Controle de branches protegidas

Impedir commits diretos em branches de longa vida — force branch workflow:

#!/bin/bash
# hooks/protect-branches.sh
 
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
 
# Só age em git commit
echo "$COMMAND" | grep -qE "^git commit" || exit 0
 
BRANCH=$(git branch --show-current 2>/dev/null || echo "")
PROTECTED_BRANCHES=("main" "master" "develop" "release" "production" "staging")
 
for protected in "${PROTECTED_BRANCHES[@]}"; do
  if [[ "$BRANCH" == "$protected" ]]; then
    echo "SEGURANÇA: commit direto em $BRANCH bloqueado." >&2
    echo "Crie uma feature branch: git checkout -b feat/sua-tarefa" >&2
    exit 1
  fi
done
 
exit 0

Camada 5 — Auditoria de segurança

Logar todas as ações sensíveis para registro imutável — independente de bloqueio:

#!/bin/bash
# hooks/security-audit.sh
 
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
USER=$(whoami)
PROJECT=$(basename "$(pwd)")
 
IS_SENSITIVE=false
 
case "$TOOL" in
  Bash)
    echo "$COMMAND" | grep -qiE "(git push|git commit|rm |mv |npm publish|deploy|kubectl|terraform)" \
      && IS_SENSITIVE=true
    ;;
  Edit|Write)
    echo "$FILE" | grep -qiE "\.(env|json|yaml|yml|toml|sh|tf|config)" \
      && IS_SENSITIVE=true
    ;;
esac
 
if [[ "$IS_SENSITIVE" == "true" ]]; then
  SUBJECT="${COMMAND:-$FILE}"
  echo "$TIMESTAMP | $USER | $PROJECT | $TOOL | $SUBJECT" >> ~/.claude/security-audit.log
fi
 
exit 0  # Auditoria nunca bloqueia

Configuração completa em camadas

// ~/.claude/settings.json (global — aplica em todos os projetos)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/security-audit.sh" },
          { "type": "command", "command": "~/.claude/hooks/protect-files.sh" },
          { "type": "command", "command": "~/.claude/hooks/protect-git-history.sh" }
        ]
      }
    ]
  }
}
// .claude/settings.json (projeto — específico do contexto)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/detect-credentials-on-commit.sh" },
          { "type": "command", "command": ".claude/hooks/protect-branches.sh" }
        ]
      }
    ]
  }
}

A auditoria fica global (todos os projetos). Os hooks específicos de workflow git ficam no projeto (onde as políticas de branch são definidas).


Tabela de defesa em profundidade

CamadaHook typeMatcherO que protege
Arquivos sensíveisPreToolUseEdit, Write, Read.env, .pem, .key, credenciais
Credenciais em códigoPreToolUseBashgit add/git commit com secret no staged
Histórico gitPreToolUseBashforce push, reset —hard, rebase em main
Branches protegidasPreToolUseBashcommit direto em main/master/develop
AuditoriaPreToolUse"" (tudo)Log imutável de todas as ações sensíveis

Armadilhas comuns

Falsa sensação de segurança

Hooks protegem contra ações do agente via Claude Code. Não protegem contra você mesmo executando git push --force no terminal. São política para o agente, não para o shell.

Hooks sem logging

Bloquear sem logar significa que você não sabe o que o agente tentou fazer. Inclua auditoria como primeiro hook da cadeia (exit 0, só loga) antes dos bloqueios.

Detect-credentials como PostToolUse

Se você configurar detecção de credenciais em PostToolUse do Bash, o commit já aconteceu quando o hook roda. Use PreToolUse para interceptar antes.

Hooks muito específicos de um projeto commitados globalmente

Um hook que bloqueia git commit em main pode causar problemas em projetos que usam main como branch de trabalho legítima.


Checklist — segurança com hooks

  • PreToolUse para arquivos sensíveis (.env, .pem, credenciais) com Read bloqueado também
  • Detecção de credenciais em staged content (PreToolUse em git add/git commit)
  • Proteção de histórico git (force push, reset —hard, rebase em main)
  • Proteção de branches protegidas (commits diretos)
  • Auditoria como primeiro hook da cadeia (exit 0, só loga)
  • Mensagens de erro incluem a alternativa segura
  • Scripts de hook com chmod +x
  • Testados com JSON de exemplo antes de ativar
  • Logs não contêm dados sensíveis (logar apenas metadata, não conteúdo)
  • Hooks globais separados dos hooks de projeto (global = universal, projeto = específico)

Ordem dos hooks na cadeia importa

Quando múltiplos hooks rodam em sequência para o mesmo matcher, a ordem define o comportamento:

1. security-audit.sh    → exit 0 (sempre — loga antes de qualquer bloqueio)
2. protect-files.sh     → exit 0 ou exit 1
3. protect-git-history.sh → exit 0 ou exit 1
4. detect-credentials.sh  → exit 0 ou exit 1

A auditoria deve ser sempre o primeiro hook. Se vier depois de um hook de bloqueio, ações bloqueadas não são registradas — você perde o rastro do que o agente tentou fazer e foi impedido.

O bloqueio mais específico deve vir antes do mais genérico. Se protect-files.sh bloqueia .env, e guardrails.sh também bloqueia .env — não há problema, o primeiro que bloqueia vence. Mas organizações claras de responsabilidade facilitam manutenção.


Casos práticos

As camadas acima não são exercício teórico — são resposta direta a incidentes reais de produção. Dois cenários mostram por que a defesa em profundidade importa mais do que um guardrail isolado (veja também o roadmap de segurança para times, que trata desse mesmo problema em escala de organização).

Cenário 1 — a credencial que quase foi ao GitHub público. Um dev pede ao agente para “subir essa branch com a config de integração”. O arquivo .env.local tinha ficado staged por engano numa sessão anterior, com uma chave AKIA... de produção dentro. Sem a Camada 2 (detecção de credenciais), o git commit passa, o git push passa, e a chave fica exposta no histórico de um repositório público — mesmo que alguém a remova depois, ela já vazou (rotação de credencial vira obrigatória, não opcional). Com o hook detect-credentials-on-commit.sh rodando como PreToolUse em git commit, o padrão AKIA[0-9A-Z]{16} é pego antes do commit existir — o incidente nunca acontece, e não há necessidade de reescrever histórico ou rotacionar segredo às pressas.

Cenário 2 — o force-push que apagou o trabalho de um colega. O agente está numa tarefa de “limpar o histórico de commits” e interpreta isso como “reescrever e forçar”. Sem a Camada 3 (proteção de histórico), um git push --force sobrescreve a branch remota — e os três commits que um colega tinha empurrado nas últimas duas horas somem. Recuperar exige git reflog na máquina do colega (se ainda não tiver feito git pull) ou, na pior hipótese, refazer o trabalho manualmente. Com protect-git-history.sh interceptando push.*(--force|-f) como PreToolUse, o comando é bloqueado e a mensagem de erro sugere a alternativa segura (--force-with-lease), que falha graciosamente se houver commits remotos que o agente não viu.

Nos dois casos, o padrão é o mesmo: o dano é irreversível ou caro de reverter depois que a ação acontece — por isso o hook precisa ser PreToolUse, não PostToolUse (ver Camada 2). Auditoria (Camada 5) não teria evitado nenhum dos dois incidentes — só teria registrado que aconteceram.


O que vem a seguir

Configurar as cinco camadas é necessário, mas não é suficiente — um hook com um bug no regex, um exit no lugar errado, ou uma condição que nunca dispara é pior do que não ter hook nenhum, porque cria falsa sensação de segurança (a primeira armadilha desta nota). A pergunta natural depois de escrever protect-files.sh ou detect-credentials-on-commit.sh é: como eu sei que esse hook realmente bloqueia o que eu acho que ele bloqueia? 08 - Testando hooks cobre exatamente isso — como simular o JSON de entrada, rodar o script isolado do Claude Code, e validar cada camada de defesa em profundidade antes de confiar nela em produção.


Como explicar em inglês

PortuguêsInglês
Defesa em profundidadeDefense in depth
Arquivo sensívelSensitive file / credential file
Histórico gitGit history / commit history
Branch protegidaProtected branch
AuditoriaAudit trail / audit log
Detecção de credenciaisCredential detection / secret scanning

Frases úteis:

  • “Defense in depth with hooks means not just one guardrail, but five layers: what can run, what can be edited, what can be committed, what can be pushed, and a full audit trail of sensitive actions.”
  • “Configure credential detection as PreToolUse on ‘git add’ and ‘git commit’ — not PostToolUse. PostToolUse fires after the commit already happened; you want to intercept before.”
  • “Hooks are policy for the agent, not for the shell. A developer can still run git push —force directly — hooks only govern what Claude Code does on your behalf.”

Veja também


Fontes