🛡️ Fail-open
Fail-open é a propriedade mais importante de qualquer hook ou plugin. Significa: quando o plugin falha por qualquer razão — exceção não tratada, timeout, arquivo ausente, dependência quebrada — o sistema host continua operando normalmente. O plugin não bloqueia, não trava, não interrompe o trabalho.
Como implementar fail-open
# Shell: wrap completo em try/catch equivalente
main() {
# lógica do plugin aqui
:
}
main "$@" || true # fail-open: erros não propagam
- •Exit code 0 em qualquer caminho de falha
- •Log de erro para diagnóstico sem bloquear
- •Verificar fail-open como primeiro teste após cada prompt
⚛️ Escrita atômica
Escrita atômica via write-then-rename garante que o arquivo de estado nunca existe em estado parcial. Escreva em arquivo temporário, depois renomeie. O rename no POSIX é atômico — nunca resulta em arquivo corrompido.
Write direto vs. Write-then-rename
✗ Perigoso
echo "$state" > state.json
# Se interrompido aqui:
# state.json está corrompido
✓ Seguro
echo "$state" > state.json.tmp
mv state.json.tmp state.json
# Rename é atômico no POSIX
💡 A janela de corrupção
Claude Code pode ser interrompido com Ctrl+C, timeout, ou kill a qualquer momento. A janela entre iniciar a escrita e completá-la é suficiente para corromper o arquivo. Estado corrompido frequentemente é irrecuperável — sem estado, a sessão está perdida.
🔒 Compare-and-swap (CAS)
Compare-and-swap garante que uma transição de estado só é aplicada se o estado atual ainda é o esperado. Leia, verifique, escreva. Se entre a leitura e a escrita o estado mudou, rejeite a operação.
Implementação de CAS em JSON
# Ler estado atual com versão
current=$(cat state.json)
current_version=$(echo "$current" | jq -r '.version')
expected_version=3
# Verificar versão antes de modificar
if [ "$current_version" != "$expected_version" ]; then
echo "CAS failed: state changed" >&2
exit 0 # fail-open
fi
# Aplicar transição com versão incrementada
new_state=$(echo "$current" | jq '.phase = "running" | .version += 1')
echo "$new_state" > state.json.tmp
mv state.json.tmp state.json
🔐 Lockfiles
Lockfiles garantem execução exclusiva em seções críticas. A criação de um diretório com mkdir é atômica no POSIX — apenas um processo consegue criar o diretório com sucesso. O restante falha e sabe que outro processo está executando.
Lockfile com cleanup garantido
LOCK_DIR="/tmp/plugin.lock"
# Cleanup garantido ao sair
cleanup() { rmdir "$LOCK_DIR" 2>/dev/null || true; }
trap cleanup EXIT
# Tentar adquirir lock
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
echo "Already running, skipping" >&2
exit 0 # fail-open: outro processo está executando
fi
# Seção crítica aqui
# Lock é liberado pelo trap EXIT
Stale locks: o problema mais comum
Se o processo morreu sem limpar o lock, o lockfile persiste indefinidamente. A solução é verificar se o PID registrado no lock ainda está ativo. Se não, o lock é stale e pode ser removido.
🚨 ERR trap
Bash por padrão ignora erros e continua executando. set -euo pipefail e trap ... ERR mudam isso — qualquer comando que falha dispara o trap, permitindo cleanup antes de sair.
O boilerplate completo
#!/usr/bin/env bash
set -euo pipefail
# Handler de erro
on_error() {
local exit_code=$?
local line=$1
echo "Error on line $line (exit: $exit_code)" >&2
cleanup
}
cleanup() {
rmdir "$LOCK_DIR" 2>/dev/null || true
}
trap 'on_error $LINENO' ERR
trap cleanup EXIT
# Resto do script aqui
set -euo pipefail: o que cada flag faz
- -e: sai imediatamente se qualquer comando retorna exit code não-zero
- -u: trata variáveis não definidas como erro (não expande para string vazia)
- -o pipefail: pipeline retorna exit code do último comando que falhou
📋 O checklist de segurança
As seis primitivas formam um checklist que pode ser aplicado mecanicamente a qualquer plugin. Verificar todas antes de considerar o safety pass completo.
Checklist de primitivas de segurança
✅ Resumo do Módulo 3.3
Próximo Módulo:
3.4 — Quebrar em Prompts Incrementais