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ão | Por quê |
|---|---|
rm -rf | Delete recursivo forçado |
git push --force / -f | Reescreve histórico remoto |
git reset --hard | Descarta mudanças locais |
git checkout -- | Descarta edições não commitadas |
git clean -f | Remove arquivos untracked de forma irreversível |
DROP TABLE / DROP DATABASE / TRUNCATE | Perda de schema ou dados |
DELETE FROM ... ; sem WHERE | Apaga todas as linhas |
chmod -R 777 | Recursão world-writable |
find ... -delete | Deleção em massa a partir de resultados do find |
npm uninstall -g | Remove pacote instalado globalmente |
curl ... | bash | Executa 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:
rm -rf /tmp/cc-mockups-xyz # liberado, sem marcadorrm -rf /tmp/a /tmp/b # liberado — todos os alvos sob /tmpO 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 bloqueado | Por quê |
|---|---|
rm -rf /tmp/x /home/leo/proj | um alvo fora do temp root |
rm -rf /tmp/../etc | path 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 /etc | composição de shell (;, &&, |, …) |
rm -rf /tmp/$(cmd) / rm -rf $TMPDIR/x | expansã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:
rm -rf node_modules # destructive-guard-ok: regenerated from package.jsonO 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: truedestructiveGuard: falseDesabilitar 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.