Pular para o conteúdo

Guard de ações destrutivas

destructive-guard é o hook que pega a classe de erro de agente por descuido: um rm -rf node_modules digitado sem pensar, um git push --force que sobrescreve commits de outra pessoa, um DELETE FROM users; sem cláusula WHERE. Ele roda como hook PreToolUse em chamadas Bash, sai com código 2 para bloquear e imprime uma única linha explicando o motivo.

Por que isso existe

O system prompt do Claude Code avisa o agente sobre ações destrutivas, mas o aviso é um prompt — pode ser sobrescrito, perdido em compactação ou simplesmente ignorado em --permission-mode=bypassPermissions. Outros agentes (Copilot, Codex, Gemini, OpenCode) não trazem nenhum aviso equivalente.

O guard é a camada abaixo de tudo isso: uma porta regex incondicional. O agente não consegue convencer o runtime a aceitar a chamada. Essa assimetria — conselho de prompt vs. recusa em runtime — é todo o propósito de ter uma camada de hooks.

Padrões bloqueados

PadrãoPor quê
rm -rfDelete recursivo forçado
git push --force / -fReescreve histórico remoto
git reset --hardDescarta mudanças locais
git checkout --Descarta edições não commitadas
git clean -fRemove arquivos untracked de forma irreversível
DROP TABLE / DROP DATABASE / TRUNCATEPerda de schema ou dados
DELETE FROM ... ; sem WHEREApaga todas as linhas
chmod -R 777Recursão world-writable
find ... -deleteDeleção em massa a partir de resultados do find
npm uninstall -gRemove pacote instalado globalmente
curl ... | bashExecuta script remoto não verificado

Um DELETE FROM sessions WHERE expired_at < now(); legítimo não é bloqueado — o regex exige especificamente uma cláusula WHERE ausente para DELETE. O objetivo é pegar descuidos, não tornar SQL válido impossível.

Carve-out de diretório temporário

Agentes geram descartáveis o tempo todo — mockups HTML, snapshots, diretórios de rascunho — sob /tmp e /var/tmp, e depois limpam com rm -rf. Exigir um marcador pra cada limpeza dessas é pura fricção, então um rm -rf limpo confinado inteiramente a um temp root é liberado sem marcador:

Terminal window
rm -rf /tmp/cc-mockups-xyz # liberado, sem marcador
rm -rf /tmp/a /tmp/b # liberado — todos os alvos sob /tmp

O carve-out é deliberadamente estrito. Seu invariante: nunca liberar um comando que possa deletar qualquer coisa fora de /tmp. Como o guard lê uma string mas o rm age no filesystem vivo, casar prefixo estaticamente não basta — ., globs e symlinks resolvem em runtime. Então vale só pra uma invocação rm única e literal, e ele resolve cada alvo com realpath antes de liberar. Qualquer coisa que introduza dúvida cai pro bloqueio:

Ainda bloqueadoPor quê
rm -rf /tmp/x /home/leo/projum alvo fora do temp root
rm -rf /tmp/../etcpath traversal
rm -rf /tmp / rm -rf /tmp/ / rm -rf /tmp/.resolve pro temp root puro
rm -rf /tmp/* / /tmp/? / /tmp/[a-z]um glob — não confinável estaticamente
rm -rf /tmp/link/ (link → fora)symlink resolvido pra fora de /tmp
rm -rf /tmp/x && rm -rf /etccomposição de shell (;, &&, |, …)
rm -rf /tmp/$(cmd) / rm -rf $TMPDIR/xexpansão de comando / variável
rm -rf /tmp/octopus-handoff-*artefatos reservados do Octopus (/tmp/octopus-*)
sudo rm -rf /tmp/x / /bin/rm …não é um rm puro

Artefatos reservados são excluídos porque o próprio Octopus escreve em /tmp — o context-handoff salva o documento de handoff do sucessor em /tmp/octopus-handoff-<timestamp>.md, então varrer o temp inteiro não é seguro. Os outros padrões destrutivos (git push --force, DROP TABLE, …) não têm carve-out de temp.

Como fazer bypass

Quando a ação destrutiva é genuinamente intencional, adicione o marcador inline:

Terminal window
rm -rf node_modules # destructive-guard-ok: regenerated from package.json

O motivo precisa ser não vazio. Por que um marcador inline em vez de uma variável de ambiente ou um toggle de configuração?

  • Visibilidade. O marcador aparece na conversa, no histórico de comandos e em qualquer diff de PR que inclua a mudança. Overrides silenciosos escondem o momento perigoso.
  • Por comando. Desabilitar globalmente não protegeria nada pelo resto da sessão; por comando mantém o guard armado para tudo o mais.
  • Amigável para auditoria. Um revisor lendo o transcript ou log vê a ação e a justificativa juntas.

Como desabilitar

Para repos com proteções out-of-band mais fortes (produção read-only, sandboxes efêmeros, branch protection que já proíbe force-push), opte por sair em .octopus.yml:

hooks: true
destructiveGuard: false

Desabilitar o guard não afeta nenhum outro hook.

Limites conhecidos

O guard é um regex sobre a string de comando do Bash. Agentes que ofuscam — codificando o comando em base64, passando por eval, construindo a string destrutiva a partir de variáveis, chamando um one-liner em Python — passam por cima dele. Essa é uma limitação conhecida; derrotar um adversário determinado é tarefa do sandbox, não do hook. O valor do hook é parar o caso comum de descuido, que é de longe o modo de falha dominante na prática.

Estendendo

Cada padrão é um regex em hooks/pre-tool-use/destructive-guard.sh, armazenado como um array. Adicionar um padrão é uma mudança de duas linhas: append no array, adicione um caso de teste em tests/test_destructive_guard.sh. Prefira padrões mais estreitos que casem precisamente com a forma perigosa em vez de padrões amplos que pegam matches incidentais — falsos positivos corroem a confiança no guard e levam os times a desabilitá-lo por completo.

Fonte: hooks/pre-tool-use/destructive-guard.sh