Claude Code: um hook de 20 linhas que impede o agente de editar seu .env
Quem usa o Claude Code no modo que aceita edições sem perguntar já deve ter pensado: "e se ele resolver mexer no .env?". Pedir isso no CLAUDE.md ajuda, mas é só uma instrução para o modelo, que pode ser esquecida. Um hook roda antes da ferramenta e o Claude Code respeita a resposta dele, então a regra deixa de depender da memória do modelo.
Como funciona
O evento PreToolUse dispara antes de cada chamada de ferramenta. O hook recebe um JSON pela entrada padrão (o caminho do arquivo vem em tool_input.file_path nas ferramentas Edit e Write; no MultiEdit, presente em algumas versões, o hook usa o mesmo campo) e pode responder com permissionDecision:
deny: bloqueia e mostra o motivo para o Claude;ask: pede sua confirmação;- sem saída e
exit 0: segue normalmente.
O script
Salve em .claude/hooks/guard-files.sh e rode chmod +x .claude/hooks/guard-files.sh (precisa do jq):
#!/usr/bin/env bash
set -uo pipefail
command -v jq >/dev/null || { echo "jq não instalado; bloqueando por segurança" >&2; exit 2; }
input=$(cat)
path=$(jq -r '.tool_input.file_path // empty' <<<"$input")
[ -z "$path" ] && exit 0
base=$(basename "$path")
decide() {
jq -nc --arg d "$1" --arg r "$2" \
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$d,permissionDecisionReason:$r}}'
exit 0
}
case "$base" in
.env.example|.env.sample|.env.template) ;;
.env|.env.*|.envrc) decide deny "$base guarda segredos. Edite à mão; use .env.example." ;;
*.pem|*.key|id_rsa|id_ed25519) decide deny "$base parece uma chave privada." ;;
package-lock.json|pnpm-lock.yaml|yarn.lock) decide ask "$base deve mudar pelo gerenciador de pacotes." ;;
esac
exit 0
E registre em .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-files.sh" }
]
}
]
}
}
Testando sem abrir o Claude
Dá para simular a entrada:
echo '{"tool_input":{"file_path":"/proj/.env.local"}}' | .claude/hooks/guard-files.sh
# {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",...}}
echo '{"tool_input":{"file_path":"/proj/src/app.ts"}}' | .claude/hooks/guard-files.sh
# (nada) -> segue normal
Detalhes que pegam
Atualização (v2): O .envrc do direnv não casa com .env.* e costuma ter segredo, por isso entrou no padrão. E o matcher inclui MultiEdit, que em algumas versões do Claude Code também edita arquivos. (Valeu, plugandship!)
- Exit 2 vs JSON. Exit 2 bloqueia usando o stderr como motivo; para responder em JSON, saia com 0. Se o script quebrar com outro código, a ação não é bloqueada, por isso o teste do
jqno topo sai com 2 (falha fechada). - Isso só cobre escrita. O Claude ainda pode ler o
.envcomReadou comcatnoBash. Para leitura, use tambémpermissions.denycomRead(./.env)eRead(./.env.*)nosettings.json, e um hook com matcherBashse quiser barrar comandos. - Proteja o próprio hook. Se o agente pode editar
.claude/settings.json, ele pode desligar a regra. Vale incluir esse arquivo e a pasta.claude/hooks/nodeny.
Documentação oficial: https://docs.anthropic.com/en/docs/claude-code/hooks
Transparência: eu mantenho o Claude Code Power Kit, um pacote pago de hooks, agentes e configurações para Claude Code (inclui uma versão mais completa deste guard). Quem quiser só gerar um CLAUDE.md para o projeto tem um gerador grátis: https://claude-md-generator.surge.sh. O kit: https://pay.kiwify.com.br/ppuZGsg