Tu agente puede destrozar una base de datos SQLite en uso — ahora Hermes rechaza la escritura

Es un miércoles por la tarde y le pides a Hermes que retoque un pequeño script de configuración en uno de tus proyectos. En ese mismo directorio resulta que hay un kanban.db, o el app.db de tu aplicación. Edita el script en unos segundos y sigues con lo tuyo. A la mañana siguiente la aplicación no arranca: database disk image is malformed. Nunca borraste la base de datos. Nunca la editaste. Ni siquiera la abriste. Lo que la destrozó fue el asistente que contrataste: su write_file trató el archivo de registro de la base de datos como un archivo de texto cualquiera.
Eso no es hipotético. Pasó dentro del propio repositorio de Hermes: el kanban.db de un equipo pasó de 550 KB a un campo de caracteres de reemplazo U+FFFD, y SQLite declaró la imagen malformada e irrecuperable. El PR #114220, fusionado el 17 de septiembre de 2026, cierra ese agujero. Este artículo repasa cómo se produjo el daño, qué protege exactamente el arreglo y si la versión que ejecutas ahora mismo ya lo tiene.
Cómo ocurrió: una herramienta de texto escribió en un archivo binario
write_file y patch son herramientas para texto. Decodifican un archivo a una cadena al leerlo y lo codifican de vuelta al escribirlo. Con texto plano ese ida y vuelta no pierde nada. Con contenido binario es catastrófico: los bytes que no son UTF-8 válido se convierten en U+FFFD (“�”) a la entrada, y esos bytes desaparecen para siempre a la salida.
Lo que resulta de verdad inquietante es que Hermes nunca decidió escribir una base de datos. Quería escribir algún archivo vecino, o sobrescribió lo que creía que era un archivo de texto mientras ordenaba un directorio. La pregunta que importa es cómo decide una herramienta “esto es binario, no lo toques”:
tools/binary_extensions.py
La comprobación antigua miraba solo el sufijo final de la ruta:
app.db -> .db -> en la lista de binarios -> rechazado OK
state.db-wal -> .db-wal -> no está en la lista -> tratado como texto
El propio .db ya estaba cubierto por la protección de sobrescritura. Los archivos sidecar no lo estaban. Así que ¿qué son?
x.db-wal— el write-ahead log, el borrador del modo WAL. Las escrituras nuevas aterrizan aquí primero y se fusionan después con el archivo principal. Si lo corrompes, has destrozado el libro de asientos que aún no se ha contabilizado.x.db-shm— el índice en memoria compartida del WAL, que los procesos usan para ponerse de acuerdo sobre las posiciones de lectura y escritura.x.db-journal— el journal de rollback que se usa en modo no-WAL para deshacer una transacción sin terminar.
Dicho sin tecnicismos: la base de datos principal es el libro mayor encuadernado y el WAL es la pila de resguardos sin contabilizar sobre la mesa. Si embadurnas los resguardos, a la mañana siguiente el libro entero deja de cuadrar. Por eso la base afectada no “perdía unas cuantas filas”, sino que quedó declarada malformada sin más.
Qué hace el arreglo: un único veredicto compartido por todas las rutas de lectura y escritura
El enfoque del PR #114220 es limpio: no parchear el síntoma, sino convertir “esto es un sidecar de SQLite” en un veredicto único y compartido, para que lecturas y escrituras usen la misma regla de medir.
En concreto, el cambio es este:
- El reconocimiento de sidecars se traslada a
tools/binary_extensions.py—un pequeño helper_lower_suffixmás una comprobaciónis_sqlite_sidecar— y tanto la protección deread_filecomofile_tools.pyy los cinco puntos de lectura del sandbox entools/file_operations.pyconsultan ese veredicto. La protección de lectura, la de escritura y las lecturas del sandbox ya no pueden discrepar. - Una ruta de sidecar se rechaza aunque el archivo todavía no exista. Después de un checkpoint una base puede no tener ningún sidecar en disco, pero la siguiente escritura crea uno. Una protección que solo comprobara si el archivo existe se colaría justo por esa ventana.
- Sobrescribir un archivo binario existente sigue rechazándose, mientras que crear un archivo
.dbtotalmente nuevo vuelve a estar permitido: el repositorio tiene fixtures de texto llamados literalmente*.db, y una prohibición general de crearlos los rompería. (Esto difiere a propósito de la política del contribuidor original; la revisión upstream la relajó.) read_fileya no devuelve texto con pérdidas para un sidecar; se trata como binario.
Upstream midió el comportamiento contra una base de datos real:
| Acción | Antes | Después |
|---|---|---|
write_file en state.db-wal (10 bytes) |
la escritura se realizaba y después SQLite informaba de disk I/O error |
rechazada, bytes intactos, integrity_check pasa |
write_file de un notes.db nuevo |
permitido | sigue permitido |
read_file de un sidecar |
devolvía texto con pérdidas | se trata como binario |
Veintiún casos de prueba cubren las cuatro combinaciones (sidecar existente / sidecar ausente / el propio .db / la herramienta patch) y todos pasan. Aquí hay también un detalle de proceso útil: esto es un rescate del PR comunitario #114091. El autor original abrió el PR, la revisión upstream decidió no fusionarlo tal cual, así que los commits se aplicaron con cherry-pick conservando la autoría y se ampliaron. Ese patrón está por todo el repositorio de Hermes y es buena parte de cómo docenas de arreglos aterrizan en un solo día.
¿Lo tiene tu versión?
Con franqueza: no. El commit se fusionó en main el 17 de septiembre de 2026, y la última release etiquetada es la v0.21.3 (tag v2026.9.14, 14 de septiembre): este arreglo llega después. Comprueba qué ejecutas:
hermes --version
hermes update
Hasta que la protección llegue a tu build, tres hábitos prácticos:
- Mantén las bases de datos vivas fuera del directorio de trabajo del agente. Saca los archivos de base de datos, los directorios de copias de seguridad y los sidecars
*-waldel árbol del proyecto, o al menos de las rutas donde dejas que el agente escriba libremente. - Cambia una base de datos con la CLI
sqlite3, no con herramientas de texto. Operaciones seguras como.backup(copia en caliente) oVACUUM INTO 'copy.db'(una copia compacta) son la propia base gestionando sus bytes: no hay ida y vuelta por texto donde perder información. - Haz copias de seguridad como rutina. Una base dañada de esta forma es esencialmente irrecuperable: el
kanban.dbde 550 KB del informe original fue una pérdida total, porque el daño ocurrió en el paso de leer-sustituir-escribir: lo que se destruyó fueron los datos en sí, no un índice.
La lección general detrás de la protección
Da un paso atrás y la regla vale para cualquier cadena de herramientas de agente: una herramienta de texto no puede hacer ida y vuelta con contenido binario, así que aceptar el objetivo ya es el defecto, no algo que salga mal durante la escritura. La reparación correcta no es una conversión más ingeniosa en el momento de escribir; es rechazar el objetivo en la entrada. Hermes ya detectaba binarios en cuatro rutas de lectura; las de escritura simplemente nunca recibieron el mismo trato. Los agujeros así viven en ese tipo de costura, donde una ruta se quedó desalineada.
Un seguimiento conocido se quedó fuera de este PR: la detección por magic bytes para bases de datos sin extensión (un archivo SQLite empieza por SQLite format 3\0). Un archivo llamado backup y sin extensión sigue tratándose como texto hoy. Si eso encaja con tu forma de nombrar archivos, el hábito 1 de arriba —mantenerlo fuera del directorio de trabajo— es tu único seguro.
También pertenece a una familia de problemas que ya hemos tratado: escritores concurrentes sobre la misma base de datos, handles de WAL retenidos por otro proceso y hermes doctor --fix actuando sobre una base que debería haber dejado en paz. Todos apuntan a la misma conclusión: cuanto más capaz es el agente, más necesitan las cosas que no debe tocar una protección legible por máquina, en lugar de depender de que tú te acuerdes de avisarle. Un rechazo en la capa de herramientas vale más que una línea en un prompt pidiéndolo por favor.
Resumen
- El defecto:
binary_extensions.pycomparaba solo el sufijo final, así que.db-walse leía como texto normal: se decodificaba a mojibake y se reescribía, destruyendo bases de datos en uso. - El arreglo (PR #114220, fusionado el 17 de septiembre de 2026): un veredicto compartido
is_sqlite_sidecarque usan las rutas de lectura, escritura y sandbox; los sidecars se rechazan tanto si existen como si no; las sobrescrituras de binarios se rechazan mientras crear sigue permitido. - Estado: en
main, todavía no en ninguna release etiquetada (la última es la v0.21.3). - Qué puedes hacer hoy: sacar las bases de datos vivas de ahí, editarlas con la CLI
sqlite3, hacer copias de seguridad.
Lectura relacionada: las notas de la versión v0.21.3 del sitio cubren esta misma familia de trabajo sobre seguridad al escribir en bases de datos; si tu base ya está dañada y necesitas rescatarla, consulta la guía de recuperación de corrupción de state.db; para saber cuándo el consejo de hermes doctor sobre un WAL pasa de útil a dañino, lee el aviso honesto sobre el WAL; y la versión del lado de contenedores del mismo tema es Docker y state.db WAL entre VMs.