O Claude Code executa comando de terminal sozinho, dentro do fluxo normal de trabalho — e às vezes esse comando é destrutivo (rm -rf de alguma coisa que não devia). Hooks resolvem isso de um jeito que instrução em texto não resolve: rodam de forma determinística, sempre, antes da ferramenta executar — não dependem do modelo "lembrar" de ser cuidadoso.
O que é um hook, na prática
Um hook é um comando, endpoint HTTP ou prompt que o Claude Code dispara sozinho em pontos específicos do ciclo de vida de uma sessão — antes de uma ferramenta rodar (PreToolUse), depois que ela roda (PostToolUse), quando você envia uma mensagem (UserPromptSubmit), no início/fim de sessão, entre outros. O evento mais útil pra segurança é o PreToolUse: ele roda antes da ação acontecer e pode bloquear.
A configuração fica num JSON — .claude/settings.json (do projeto, versionável) ou ~/.claude/settings.json (só sua máquina). Quando o evento dispara, o Claude Code manda o contexto via stdin em JSON:
{
"session_id": "abc123",
"cwd": "/home/user/meu-projeto",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/build"
}
}O hook decide o resultado pelo código de saída: 0 deixa passar, 2 bloqueia (o texto mandado pro stderr vira o motivo mostrado). Existe também uma forma de responder com JSON estruturado no lugar do código de saída simples, mas pra um bloqueio direto o código 2 já resolve.
Um hook real: bloquear rm -rf perigoso
Abaixo está um hook completo, testado, que bloqueia rm -rf /, rm -rf ~ e rm -rf * antes de rodarem — sem depender de jq (só Python, que já vem em praticamente qualquer máquina de desenvolvedor, inclusive Windows).
1. O script do hook
import json
import re
import sys
DANGEROUS_PATTERNS = [
r"rm\s+-rf\s+/(?:\s|$)",
r"rm\s+-rf\s+~(?:\s|$)",
r"rm\s+-rf\s+\*",
]
def main() -> int:
try:
payload = json.load(sys.stdin)
except json.JSONDecodeError:
return 0
command = payload.get("tool_input", {}).get("command", "")
for pattern in DANGEROUS_PATTERNS:
if re.search(pattern, command):
sys.stderr.write(f"Comando bloqueado por parecer destrutivo: {command!r}\n")
return 2
return 0
if __name__ == "__main__":
sys.exit(main())2. Registrar no settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/block_rm.py"
}
]
}
]
}
}matcher: "Bash" restringe o hook só à ferramenta Bash — sem isso ele rodaria (inutilmente) antes de toda ferramenta.
Testando antes de confiar
Testado neste ambiente, direto — sem precisar abrir o Claude Code, só simulando o stdin que ele manda de verdade:
echo '{"tool_input": {"command": "rm -rf /"}}' | python3 .claude/hooks/block_rm.py
echo "saiu com: $?"
# saiu com: 2 (bloqueado)
echo '{"tool_input": {"command": "ls -la"}}' | python3 .claude/hooks/block_rm.py
echo "saiu com: $?"
# saiu com: 0 (liberado)Os quatro casos de teste — rm -rf /, rm -rf ~, um comando inofensivo, e um rm -rf ./build (que deve passar, porque apagar uma pasta específica do projeto é uma operação normal, não uma tentativa de destruir tudo) — se comportaram exatamente como esperado.
Onde isso para de ser suficiente
Esse hook cobre o caso mais óbvio (comando digitado direto), mas não é uma sandbox de verdade — um comando ofuscado ou passado por variável pode escapar de um regex simples. Pra proteção séria, combine hooks com permissões restritivas do próprio Claude Code e, quando possível, rode em ambiente isolado/descartável pra qualquer coisa realmente arriscada.
Download
O script completo, o settings.json pronto e um README com instruções estão empacotados no arquivo abaixo, prontos pra copiar pro seu projeto.
Fonte oficial
code.claude.com/docs/en/hooks — documentação completa de hooks, com todos os eventos disponíveis e o schema inteiro de entrada/saída.
