PreToolUse — interceptar e validar antes de executar
TL;DR
PreToolUse é o hook que executa antes de qualquer tool call. É o ponto de controle principal do Claude Code: intercepta, valida, e pode bloquear. Exit code 0 = aprovado, exit code não-zero = bloqueado. O agente recebe o resultado do hook e decide como proceder. É onde guardrails, auditoria e aprovação humana são implementados.
Como funciona
Quando o agente decide executar uma tool call (ex: Bash("git push --force origin main")), o runtime:
- Serializa o input como JSON e passa para o hook via stdin ou variável de ambiente
- Executa o hook command
- Se exit code = 0: executa a tool
- Se exit code ≠ 0: bloqueia a execução e retorna o stderr do hook ao agente como mensagem de erro
O agente recebe o bloqueio como feedback e pode tentar uma abordagem alternativa.
Estrutura do input
O hook recebe via stdin um JSON com o input da tool call:
{
"tool_name": "Bash",
"tool_input": {
"command": "git push --force origin main"
}
}Para Edit:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/projeto/src/config/database.ts",
"old_string": "password: 'prod_secret'",
"new_string": "password: process.env.DB_PASSWORD"
}
}Bloqueio simples — exit 1
O hook mais simples: bloquear um padrão e retornar mensagem de erro.
#!/bin/bash
# hooks/block-force-push.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
if echo "$COMMAND" | grep -q "push --force\|push -f"; then
echo "BLOQUEADO: force push não permitido. Use --force-with-lease ou abra PR." >&2
exit 1
fi
exit 0Configuração:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/block-force-push.sh" }]
}
]
}
}Bloqueio por padrão de arquivo
Proteger arquivos sensíveis de edição:
#!/bin/bash
# hooks/protect-sensitive-files.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
PROTECTED_PATTERNS=(
".*\.env$"
".*credentials.*"
".*\.pem$"
".*secrets.*"
)
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if echo "$FILE" | grep -qE "$pattern"; then
echo "BLOQUEADO: $FILE é um arquivo protegido. Edite manualmente." >&2
exit 1
fi
done
exit 0Logging de auditoria
Hook que não bloqueia, só registra:
#!/bin/bash
# hooks/audit-log.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // .tool_input.file_path // ""')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
SESSION_ID="${CLAUDE_SESSION_ID:-unknown}"
echo "$TIMESTAMP | $SESSION_ID | $TOOL | $COMMAND" >> ~/.claude/audit.log
exit 0O arquivo ~/.claude/audit.log acumula todas as tool calls da sessão — útil para debugging e compliance.
Aprovação humana interativa
Para comandos de alto risco, pedir aprovação antes de executar:
#!/bin/bash
# hooks/require-approval.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
HIGH_RISK_PATTERNS=(
"rm -rf"
"DROP TABLE"
"DELETE FROM"
"git push"
"kubectl delete"
)
for pattern in "${HIGH_RISK_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "APROVAÇÃO NECESSÁRIA: $COMMAND"
echo "Confirma? (s/N): " >&2
read -r response < /dev/tty
if [[ ! "$response" =~ ^[Ss]$ ]]; then
echo "Bloqueado pelo usuário." >&2
exit 1
fi
break
fi
done
exit 0Aprovação interativa só funciona em sessão interativa
Em modo headless (CI/CD,
Delegação a outro LLM
Para validações complexas que precisam de raciocínio:
#!/bin/bash
# hooks/llm-validator.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# Delegar a decisão a outro LLM
DECISION=$(echo "$COMMAND" | claude --print \
"Este comando bash é seguro para executar num servidor de produção?
Responda apenas: SAFE ou UNSAFE: MOTIVO" \
--max-tokens 50)
if echo "$DECISION" | grep -q "^UNSAFE"; then
MOTIVO=$(echo "$DECISION" | sed 's/^UNSAFE: //')
echo "LLM bloqueou: $MOTIVO" >&2
exit 1
fi
exit 0Ver 06 - Delegar permissão para o padrão completo.
Modificação de input
O hook pode modificar o input antes de executar, retornando JSON via stdout:
#!/bin/bash
# hooks/sanitize-rm.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# Adicionar --interactive em todo rm
if echo "$COMMAND" | grep -q "^rm "; then
SAFE_COMMAND=$(echo "$COMMAND" | sed 's/^rm /rm -i /')
echo '{"decision": "approve", "modified_input": {"command": "'"$SAFE_COMMAND"'"}}'
exit 0
fi
exit 0Múltiplos hooks em sequência
Quando há múltiplos hooks configurados para o mesmo matcher, todos executam em sequência. Se qualquer um retornar exit ≠ 0, a tool call é bloqueada.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/audit-log.sh" },
{ "type": "command", "command": "~/.claude/hooks/block-force-push.sh" },
{ "type": "command", "command": "~/.claude/hooks/protect-sensitive-files.sh" }
]
}
]
}
}Veja também
- 01 - Sistema de hooks — lifecycle e configuração
- 05 - Guardrails — conjunto completo de guardrails recomendados
- 06 - Delegar permissão — meta-agente para validação com LLM
- 08 - Testando hooks — como testar e debugar hooks
- Hooks e Guardrails — índice do galho