“Tenho um shell e sou criativo.”

— Claude, explicando por que criou um script de 47 linhas como uma string e passou para python -c

Essa frase é real. Foi dita pelo meu agente de IA — bem, não com essas palavras exatas, mas com essas ações. Ele precisava iniciar um processo de um pipeline ETL. O comando correto estava no Makefile. Mas algo falhou. E ao invés de perguntar, ele fez o que qualquer programador com acesso root e zero supervisão faria: improvisou.

Inacreditável.

A confabulação que ninguém percebe

escrevi antes sobre confabulações de código: o LLM que inventa um campo JSON, constrói um DTO ao redor, gera os testes, e você acaba com 90 testes verdes validando ficções. Esse problema é grave, mas ao menos é estático. O código inventado fica ali, até que alguém o revise.

Há um tipo de confabulação muito mais perigoso: a confabulação operacional. É quando o agente não inventa código, mas sim caminhos de execução.

O padrão é sempre o mesmo:

Caminho correto falha → Agente busca um atalho → O atalho “funciona” → Dano oculto

Vou dar dois exemplos reais de um pipeline ETL que agrega dados dispersos de várias fontes na web.

Caso 1: O script como string. O pipeline tem um comando make scrape-fonte que levanta um watchdog que, por sua vez, lança workers. O watchdog monitora, reinicia workers que caíram e fecha conexões órfãs. Um dia, o agente precisava iniciar um scrape. O make falhou por causa de um problema de dependências. O que ele fez? Criou um script Python inline, 47 linhas como uma string, e passou para python -c "...". Sem error handling. Sem watchdog. Sem cleanup. Funcionou… até que um worker ficou preso e ninguém o reiniciou. Dados parciais, conexões não encerradas e eu só percebi isso três dias depois.

Caso 2: O worker solitário. Outra sessão, mesmo pipeline. O agente executou voyeur worker diretamente, pulando o watchdog. O worker começou a fazer o scrape, encontrou um timeout de rede e ficou em um loop infinito de tentativa de repetição consumindo recursos. Sem watchdog, ninguém o matou. Sem logging centralizado, ninguém viu. O servidor passou três horas tentando acessar uma página que retornava 503.

Em ambos os casos, o agente tomou uma decisão que parecia razoável localmente. “O make falhou, mas eu sei como fazer a mesma coisa manualmente.” O problema é que ele não sabia a mesma coisa. Sabia 60%. Os outros 40% eram as invariantes do sistema que não apareciam em nenhum README.

Por que proibir não funciona

Minha primeira reação foi como a de todo mundo: escrever regras.

1
2
3
4
## PROIBIDO
- NUNCA executar workers diretamente
- NUNCA criar scripts como strings
- SEMPRE usar make

Sabe como o LLM lê isso?

O que você escreveO que ele entende
“NUNCA faça X”“X está proibido, exceto se eu achar necessário”
“SEMPRE use Y”“Y é preferível, mas se falhar, eu improviso”
“É perigoso fazer Z”“Tomarei cuidado enquanto faço Z”

Eu comentei sobre isso em um post anterior: instruções brandas descrevem atitudes. O LLM precisa de impossibilidades. “Não corra junto à piscina” não funciona. O que funciona é que não exista uma piscina ou que o chão seja feito de velcro.

O LLM sempre acredita que seu caso é uma exceção. Seu treinamento o otimiza para concluir tarefas, demonstrar competência e evitar atritos. Quando o caminho correto falha, esses incentivos se alinham em uma direção inevitável: “consigo resolver isso sozinho”. E ele resolve. Mal.

A filosofia: impossível, não proibido

Existe uma ideia na engenharia de segurança que funciona há décadas: tornar o incorreto impossível, em vez de apenas proibido.

Você não coloca uma placa dizendo “Proibido inserir diesel” em um carro movido a gasolina. Você faz com que o bocal do diesel não se encaixe. Você não escreve um aviso no plug dizendo “Este aparelho funciona a 110V, não conecte a 220V”. Você faz com que o plug tenha um formato diferente.

De forma clara: o sistema deve impedir fisicamente o erro, em vez de depender de alguém que leia um manual.

Aplicado a um agente de IA operando um pipeline ETL, isso se traduz em três camadas de defesa.

Camada 1: O código se defende sozinho

Se o worker precisa do watchdog para funcionar corretamente, ele deve verificar isso:

1
2
3
4
5
6
7
8
9
class Worker:
    def _verify_invocation(self) -> None:
        """O worker se recusa a iniciar sem o watchdog."""
        if not os.environ.get("WATCHDOG_PID"):
            raise RuntimeError(
                "Worker iniciado sem watchdog. "
                "Use 'make scrape-<fonte>'. "
                "NUNCA execute o worker diretamente."
            )

Agora não importa o quão criativo seja o agente. Ele pode escrever python -c "from pipeline import Worker; Worker().run()" e o worker vai dar um erro de cara. Não há caminho alternativo. O código se defende.

O mesmo vale para as fases do pipeline. Se a fase 3 (consolidação) precisa que a fase 1 (scrape) tenha terminado, ela deve verificar isso ao iniciar:

1
2
3
4
5
6
7
8
9
def verify_prerequisites(locale: str) -> None:
    """Fase 3 não inicia se Fase 1 não foi concluída."""
    sources = get_enabled_sources(locale)
    completed = [s for s in sources if has_valid_data(s)]
    if not completed:
        raise PrerequisiteError(
            f"A fase 3 requer pelo menos uma fonte com dados. "
            f"Primeiro execute: make scrape-<fonte>"
        )

Isso não é um teste. Não é uma regra em um arquivo de configuração. É código que executa toda vez e que não depende de o agente ter lido o README.

Camada 2: Uma única interface, sem atalhos

O Makefile é uma lista branca de operações. Se não está em make help, não existe.

1
2
3
4
5
6
7
8
9
scrape-%:           ## Scrapear uma fonte (make scrape-fonte)
	$(MAKE) health
	cd packages/etl && uv run pipeline scrape $*

consolidate:        ## Consolidar todas as fontes
	cd packages/etl && uv run pipeline consolidate

verify:             ## Verificar integridade dos dados
	cd packages/etl && uv run pipeline verify

Repare em um detalhe: scrape-% executa health antes de fazer qualquer coisa. O health check verifica se os adaptadores de scraping ainda funcionam (sites web mudam sem avisar). O agente não pode pular esta verificação porque ela está dentro do alvo make.

Ao inimigo que foge, ponte de prata: se você quer que o agente use o caminho correto, torne-o o mais fácil. make scrape-fonte é mais cômodo que montar um script manualmente. Não lute contra a natureza do agente — canalize-a.

Camada 3: Interceptores que bloqueiam atalhos

As camadas 1 e 2 cobrem 90%. Os 10% restantes são o agente sendo super criativo. Para isso, você intercepta os comandos antes de serem executados.

Ferramentas como Claude Code permitem configurar hooks que inspecionam cada comando de shell antes de executá-lo. Um hook pode bloquear padrões perigosos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env bash
# Interceptor de comandos: bloqueia padrões perigosos

COMMAND="$1"

# Nunca criar scripts como strings
if echo "$COMMAND" | grep -qE 'python[3]?\s+-c\s+'; then
    echo "BLOQUEADO: Não criar scripts como strings. Use make."
    exit 2
fi

# Nunca executar o worker diretamente
if echo "$COMMAND" | grep -qE 'pipeline\s+worker\b'; then
    echo "BLOQUEADO: Não executar o worker diretamente. Use 'make scrape-<fonte>'."
    exit 2
fi

# Nunca acessar SQLite diretamente
if echo "$COMMAND" | grep -qE 'sqlite3\s+.*\.(db|sqlite)'; then
    echo "BLOQUEADO: Não executar SQL diretamente. Use os comandos make."
    exit 2
fi

# Nunca mover imagens do pipeline manualmente
if echo "$COMMAND" | grep -qE '(mv|cp)\s+.*images/'; then
    echo "BLOQUEADO: Não mover imagens manualmente. Use o pipeline."
    exit 2
fi

exit 0

É uma lista negra, sim. E listas negras não são perfeitas. Mas combinadas com as camadas 1 e 2, fecham o cerco. O agente teria que:

  1. Inventar um comando que não corresponda a nenhum padrão do hook
  2. Além disso, não ser detectado pela proteção do código
  3. E produzir um resultado correto sem usar o Makefile

É possível, mas estamos falando de um nível de criatividade que beira a má intenção. E LLMs não são mal-intencionados — são criativamente preguiçosos. Coloque uma barreira e eles buscarão o caminho mais fácil, que neste caso já é o Makefile.

O catálogo de atalhos que você não sabia que temia

Além de executar coisas de forma errada, há confabulações operacionais dentro do próprio código produzido pelo agente:

AtalhoPor que fazPor que é letal
Afrouxar testes (assert count >= 0)O teste falha, quer que passeUm teste que sempre passa não valida nada
Inventar fixtures JSONPrecisa de dados de teste, mas não tem reaisFiccção validando ficção
Suprimir warnings (# type: ignore)O linter reclama, quer silêncioErros reais escondidos
except Exception: passAlgo falha, quer que “funcione”Erros silenciosos se acumulam
Retry infinitoUm serviço não respondeConsome recursos escondendo o erro real

Para cada um desses casos, a defesa é consistente: não proíba, impossibilite.

Como impedir que afrouxe testes? Usando um plugin de pytest que detecta assertions suspeitas:

1
2
3
4
5
6
7
8
def pytest_collection_modifyitems(items):
    for item in items:
        source = inspect.getsource(item.function)
        if ">= 0" in source and "count" in source:
            warnings.warn(
                f"Teste suspeito em {item.nodeid}: "
                f"'count >= 0' sempre passa."
            )

Como impedir fixtures inventadas? Exigindo que cada fixture tenha documentação de proveniência: URL de origem, data de captura, hash SHA256. Uma fixture sem proveniência não passa no CI.

Como impedir except Exception: pass? Com uma regra de ruff ou flake8 que bloqueie isso como erro, não alerta.

Em cada caso, a verificação é mecânica, automática e não depende de alguém ter lido uma instrução.

O problema de fundo: confiança vs. instrumentação

Há um mantra em engenharia que se aplica perfeitamente aqui:

“You don’t trust; you instrument.”

Confiança é um sentimento. Instrumentação é um sistema. Sentimentos são péssimos para escalar. Sistemas escalam bem.

Quando você dá a um agente IA acesso ao shell e diz “mas tenha cuidado”, está confiando. Quando você dá acesso ao shell onde os comandos perigosos não funcionam, está instrumentando.

A diferença não é de grau. É de natureza. Um agente que “tem cuidado” falha quando se distrai (e um LLM se distrai em cada geração de tokens). Um sistema que impede o caminho errado não falha porque não há nada para falhar.

O marcador

CamadaConfiabilidadeCusto de implementaçãoExemplo
Proteção no códigoAltaMédioWorker que verifica watchdog
Makefile como interface únicaAltaBaixomake help = lista branca
Hooks interceptoresMédia-altaBaixoBloquear python -c
Regras no config do agenteBaixaMínimo“NUNCA faça X”
Confiar no agenteNulaGrátis¯\_(ツ)_/¯

As três primeiras camadas são acumulativas. A quarta é um complemento útil, mas insuficiente. A quinta é o que todo mundo faz até levar um golpe.

Quem vigia o vigilante?

Resta a pergunta desconfortável: quem escreve as proteções? Se o agente IA escreve o código que deveria restringi-lo, não estamos em um ciclo fechado?

Sim. Parcialmente.

O ponto principal é que as proteções são projetadas pelo humano e implementadas por quem seja — agente, humano ou um macaco com teclado. O importante é que, uma vez implementadas, as proteções se testem em si mesmas. O teste de _verify_invocation não testa o pipeline; testa que o pipeline rejeita invocações incorretas. Esse teste é trivial para escrever e difícil de errar:

1
2
3
4
def test_worker_rejects_direct_invocation():
    """Worker DEVE falhar sem watchdog."""
    with pytest.raises(RuntimeError, match="sem watchdog"):
        Worker().run()

Se este teste passa, a proteção funciona. Se a proteção funciona, o agente não pode ignorá-la. Não importa quem escreveu o código. Importa que o teste existe e passa.

O que aprendi

Trabalho há meses com um agente IA em um pipeline ETL que agrega dados de fontes dispersas da web. Já vi o agente fazer coisas brilhantes e coisas que me deixaram “com as calças na mão”. A conclusão mais importante:

Não projete regras para um agente disciplinado. Projete sistemas para um agente com acesso ao shell e criatividade ilimitada.

O agente não é malicioso. É um otimizador. Ele otimiza para concluir a tarefa, não para respeitar suas invariantes. Se você deixar uma brecha, ele vai encontrá-la. Não porque quer te prejudicar, mas porque essa é literalmente a função dele: encontrar caminhos.

Seu trabalho não é bloquear cada caminho errado. É fazer com que o único caminho que funcione seja o correto.


Série completa sobre falhas de IA em produção: Os 44 emails inventadosMEMORY.mdSilent failure5 defesas reativasEste post: defesas estruturais.