Seu agente pode destruir um banco SQLite em uso — agora o Hermes recusa a escrita

É uma quarta-feira à tarde e você pede ao Hermes que ajuste um pequeno script de configuração em um dos seus projetos. O mesmo diretório guarda um kanban.db, ou o app.db da sua aplicação. Ele edita o script em poucos segundos e você segue em frente. Na manhã seguinte a aplicação não inicia: database disk image is malformed. Você nunca apagou o banco. Você nunca o editou. Você nunca sequer o abriu. Quem o destruiu foi o assistente que você contratou — o write_file dele tratou o arquivo de log do banco como um arquivo de texto comum.
Isso não é hipótese. Aconteceu dentro do próprio repositório do Hermes: o kanban.db de uma equipe passou de 550 KB para um amontoado de caracteres de substituição U+FFFD, e o SQLite declarou a imagem malformada e irrecuperável. O PR #114220, mesclado em 17 de setembro de 2026, fecha esse buraco. Este post percorre como o dano aconteceu, o que a correção de fato protege e se a build que você está rodando agora já tem isso.
Como aconteceu: uma ferramenta de texto escreveu em um arquivo binário
write_file e patch são ferramentas para texto. Elas decodificam um arquivo em string na leitura e o codificam de volta na escrita. Para texto simples esse ciclo é livre de perdas. Para conteúdo binário ele é catastrófico: bytes que não são UTF-8 válido viram U+FFFD (“�”) na entrada, e esses bytes se perdem para sempre na saída.
O que torna isso genuinamente inquietante é que o Hermes nunca decidiu escrever em um banco de dados. Ele queria escrever em algum arquivo vizinho, ou sobrescreveu o que acreditava ser um arquivo de texto enquanto organizava um diretório. A pergunta que importa é como uma ferramenta decide “isto é binário, não mexa nisso”:
tools/binary_extensions.py
A verificação antiga olhava apenas o sufixo final do caminho:
app.db -> .db -> na lista de binários -> recusado OK
state.db-wal -> .db-wal -> fora da lista -> tratado como texto
O próprio .db já estava coberto pela proteção de sobrescrita. Os arquivos laterais (sidecars) não estavam. Então o que são eles?
x.db-wal— o write-ahead log, o rascunho do modo WAL. As novas escritas caem aqui primeiro e são mescladas de volta ao arquivo principal depois. Corrompa-o e você rasgou o livro-razão que ainda não foi lançado.x.db-shm— o índice de memória compartilhada do WAL, que os processos usam para concordar sobre posições de leitura e escrita.x.db-journal— o journal de rollback usado no modo não-WAL para desfazer uma transação inacabada.
Em termos simples: o banco principal é um livro-razão encadernado, e o WAL é a pilha de comprovantes ainda não lançados sobre a mesa. Passe tinta por cima dos comprovantes e o livro inteiro deixa de fechar na manhã seguinte. É por isso que o banco afetado não ficou “sem algumas linhas” — ele foi declarado malformado de imediato.
O que a correção faz: um único veredito, compartilhado por todos os caminhos de leitura e escrita
A abordagem do PR #114220 é limpa: não remendar o sintoma, mas transformar “isto é um arquivo lateral do SQLite” em um veredito único compartilhado, para que leituras e escritas usem a mesma régua.
Na prática, a mudança é:
- O reconhecimento de arquivos laterais foi movido para
tools/binary_extensions.py— um pequeno helper_lower_suffixmais uma verificaçãois_sqlite_sidecar— e a proteção doread_file, ofile_tools.pye os cinco pontos de leitura no sandbox emtools/file_operations.pytodos consultam essa verificação. A proteção de leitura, a de escrita e as leituras do sandbox não podem mais discordar. - Um caminho de arquivo lateral é recusado mesmo quando o arquivo ainda não existe. Depois de um checkpoint, um banco pode não ter nenhum arquivo lateral em disco, mas a escrita seguinte cria um. Uma proteção que só verifica se o arquivo já existe passaria exatamente por essa janela.
- Sobrescrever um arquivo binário existente continua sendo recusado, enquanto criar um arquivo
.dbtotalmente novo volta a ser permitido — o repositório tem fixtures de texto literalmente chamadas*.db, e uma proibição geral de criação as quebraria. (Isso difere de propósito da política do contribuidor original; a revisão upstream a afrouxou.) - O
read_filenão devolve mais texto com perdas para um arquivo lateral; ele é tratado como binário.
O upstream mediu o comportamento contra um banco real:
| Ação | Antes | Depois |
|---|---|---|
write_file em state.db-wal (10 bytes) |
a escrita passou, e então o SQLite reportou disk I/O error |
recusado, bytes intactos, integrity_check passa |
write_file de um novo notes.db |
permitido | continua permitido |
read_file de um arquivo lateral |
texto com perdas retornado | tratado como binário |
Vinte e um casos de teste cobrem as quatro combinações (arquivo lateral existente / ausente / o próprio .db / a ferramenta patch) e todos passam. Há também uma lição útil de processo aqui: isto é um resgate do PR comunitário #114091. O autor original abriu o PR, a revisão upstream optou por não mesclá-lo como estava, então os commits foram aplicados por cherry-pick com a autoria preservada e o escopo ampliado. Esse padrão está em todo lugar no repositório do Hermes, e é boa parte do motivo pelo qual dezenas de correções entram em um único dia.
Sua versão já tem isso?
Sendo honesto: não. O commit entrou na main em 17 de setembro de 2026, e a release marcada mais recente é a v0.21.3 (tag v2026.9.14, 14 de setembro) — esta correção vem depois dela. Veja o que você está rodando:
hermes --version
hermes update
Até a proteção chegar à sua build, três hábitos práticos:
- Mantenha bancos em uso fora do diretório de trabalho do agente. Mova arquivos de banco, diretórios de backup e arquivos laterais
*-walpara fora da árvore do projeto — ou pelo menos para fora dos caminhos em que você deixa o agente escrever livremente. - Altere um banco pelo CLI
sqlite3, não por ferramentas de texto. Operações seguras como.backup(backup a quente) eVACUUM INTO 'copy.db'(uma cópia compacta) são o banco gerenciando os próprios bytes; não há ciclo de ida e volta por texto em que perder informação. - Faça backup com rotina. Um banco danificado dessa forma é praticamente irrecuperável — o
kanban.dbde 550 KB do relato original foi perda total, porque o dano aconteceu na etapa de ler-substituir-escrever: o que foi destruído foram os próprios dados, não um índice.
A lição geral por trás da proteção
Dê um passo atrás e a regra vale para qualquer cadeia de ferramentas de agente: uma ferramenta de texto não consegue fazer ida e volta de conteúdo binário, então aceitar o alvo já é o defeito — não algo que dá errado durante a escrita. O conserto certo não é uma conversão mais esperta na hora de escrever; é recusar o alvo na entrada. O Hermes já farejava binários em quatro caminhos de leitura; os caminhos de escrita simplesmente nunca receberam o mesmo tratamento. Buracos assim vivem nesse tipo de costura, onde um caminho ficou desalinhado.
Um desdobramento conhecido não entrou neste PR: o sniffing por magic bytes para bancos sem extensão (um arquivo SQLite começa com SQLite format 3\0). Um arquivo chamado backup, sem extensão, ainda é tratado como texto hoje. Se isso combina com os seus hábitos de nomear arquivos, o hábito 1 acima — mantê-lo fora do diretório de trabalho — é o seu único seguro.
Ela também pertence a uma família de problemas que já abordamos antes: escritores concorrentes no mesmo banco, handles de WAL mantidos por outro processo e o hermes doctor --fix agindo sobre um banco que deveria ter deixado em paz. Todos apontam para a mesma conclusão: quanto mais capaz o agente, mais as coisas que ele não pode tocar precisam de proteção legível por máquina, em vez de depender de você lembrar de avisá-lo. Uma recusa na camada de ferramentas vale mais do que uma linha em um prompt pedindo com jeitinho.
Resumo
- O defeito:
binary_extensions.pycomparava apenas o sufixo final, então.db-walera lido como texto comum — decodificado em mojibake e gravado de volta, destruindo bancos em uso. - A correção (PR #114220, mesclado em 17 de setembro de 2026): um veredito compartilhado
is_sqlite_sidecarusado por leituras, escritas e caminhos de sandbox; arquivos laterais recusados existam eles ou não; sobrescritas binárias recusadas, enquanto a criação continua permitida. - Status: na
main, ainda não está em nenhuma release marcada (a mais recente é a v0.21.3). - O que você pode fazer hoje: mover bancos em uso para fora, editá-los com o CLI
sqlite3e fazer backups.
Leitura relacionada: as notas de release da v0.21.3 do site cobrem a mesma família de trabalho de segurança de escrita em bancos; se o seu banco já está danificado e você precisa resgatá-lo, veja o guia de recuperação de corrupção do state.db; para saber quando o conselho do hermes doctor sobre um WAL deixa de ser útil e passa a ser prejudicial, leia o aviso honesto sobre WAL; e a versão do mesmo tema no lado de contêineres é Docker e WAL do state.db entre VMs.