MÓDULO 3.3

🛡️ Fail-Open e os Padrões de Segurança

As seis primitivas de segurança que todo plugin precisa — e como verificar se o seu projeto as implementa corretamente.

📚

Tópicos

6

⏱️

Minutos

45

🛡️

Tipo

Primitivas

1

🛡️ 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
2

⚛️ 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.

3

🔒 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
4

🔐 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.

5

🚨 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
6

📋 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

1
Fail-open: Matar o processo com SIGKILL não bloqueia o sistema host
2
Escrita atômica: Nenhum write direto em arquivo de estado — sempre via tmp + rename
3
CAS: Toda transição de estado verifica a versão antes de aplicar
4
Lockfile: Seções críticas têm lock com cleanup garantido no EXIT trap
5
ERR trap: set -euo pipefail e trap ERR em todo script shell
6
Idempotência: Re-executar o mesmo prompt duas vezes não causa efeitos duplicados

Resumo do Módulo 3.3

Fail-open é inegociável — plugin que bloqueia é pior que não existir
Write-then-rename — atomicidade POSIX elimina corrupção de estado
CAS previne race conditions — versioning de estado para transições seguras
Lockfile + cleanup — mutex sem dependência externa
6 primitivas como checklist — verificação mecânica do safety pass

Próximo Módulo:

3.4 — Quebrar em Prompts Incrementais