Mover las cachés de secretos a la raíz del perfil: el agujero en la sandbox que podía abrir un renombrado de carpeta

Configuraste Hermes con Bitwarden para que obtenga él mismo la contraseña de la base de datos en lugar de pegarla tú cada vez. Siendo cuidadoso, lo metiste en una sandbox y marcaste cache/bws_cache.json como ruta denegada: al fin y al cabo, ese archivo es la caché de claves en texto plano, ya descifradas.
Y resulta que la denegación se viene abajo con un empujón: renombra la carpeta cache/ a cache2/ y el archivo bloqueado se va con ella. Léelo en la nueva ruta y el guardián ya no lo reconoce.
No es el descuido de una persona. Es un desajuste de fronteras de manual: la sandbox deniega por ruta, mientras que la carpeta padre de esa ruta está dentro del alcance de escritura del agente. El 2026-10-31 Hermes movió los cuatro archivos de caché de credenciales a la raíz del home del perfil, eliminando el atajo en lugar de parchearlo.
El problema no era «oculto demasiado poco profundo», era «oculto en una carpeta que se puede renombrar»
Conviene entenderlo bien, porque la lección sobrevive al bug.
En Windows, la sandbox MXC (la que usa el plugin OpenShell) bloquea archivos mediante deniedPaths. Ese mecanismo da por supuesto que el archivo denegado está directamente en el home concedido. Si vive dentro de un subdirectorio, y el agente puede crear, escribir y renombrar ese subdirectorio, la denegación es decorativa:
- La sandbox dice que
home/cache/bws_cache.jsonno debe leerse. - El agente tiene permiso de escritura sobre
home/cache/, así que renombra el directorio ahome/cache-old/. - El archivo se mueve con su directorio: ahora es
home/cache-old/bws_cache.json. - Esa ruta no está en la lista de denegaciones. Léelo. Ya está.
El pull request lo dice sin rodeos: el atajo se mantiene en MXC 1.0.0 y 1.0.1, según lo medido. Así que la promesa anterior —«esta ruta está denegada»— no sobrevive a la condición «el archivo denegado vive en un subdirectorio escribible», y las cuatro cachés de credenciales cumplían esa condición.
La conclusión es tajante: no puedes arreglar esto con una lista de denegaciones más larga. Los archivos tienen que dejar de vivir en un subdirectorio escribible.
Adónde fueron los cuatro archivos
| Qué guarda | Ruta antigua (dentro de un subdirectorio escribible) | Ruta nueva (raíz del home del perfil) |
|---|---|---|
| Caché en texto plano de Bitwarden | cache/bws_cache.json |
bws_cache.json |
| Caché cifrada de Bitwarden | cache/bws_cache.enc.json |
bws_cache.enc.json |
| Caché de 1Password | cache/op_cache.json |
op_cache.json |
| Registro del sidecar de Photon (puerto, token, pid) | runtime/photon-sidecar.json |
photon-sidecar.json |
Los nombres de archivo no cambian; solo cambió el nivel de directorio. No puedes moverlos renombrando una carpeta, porque no son archivos dentro de una carpeta: son hijos directos del propio home, y una sandbox que los deniega deniega esa ruta exacta.
En la misma pasada se cerró una carencia de largo recorrido: la caché de 1Password no estaba cubierta por ningún guardián antes de esto. Así que el cambio no es solo un traslado; los cuatro nombres viven ahora en una única lista compartida (SECRET_CACHE_FILES en hermes_cli/home_data_layout.py), y todos los lugares que necesitan saber «estos archivos son credenciales» leen esa única lista.
Las copias antiguas: se borran sin leerlas
La pregunta natural tras actualizar es qué ocurre con cache/bws_cache.json.
Se borra en el primer uso, sin leerlo. La razón es sencilla: esa ruta está dentro de un directorio en el que el agente puede escribir, así que su contenido ya no es de fiar. Leerlo sería invitar de vuelta a datos posiblemente manipulados. El borrado es idempotente, y el siguiente uso de Bitwarden o 1Password simplemente vuelve a descargar la clave.
El coste es mínimo porque las cuatro cachés son regenerables:
- Cachés de Bitwarden y 1Password: como mucho una descarga extra después de la actualización.
- El registro del sidecar de Photon: se reconstruye la próxima vez que se reinicie el gateway.
Hay un caso límite honesto. Un perfil que no vuelva a tocar nunca su fuente de Bitwarden o 1Password conserva el archivo antiguo en cache/, sin leer. Sigue cubierto por todos los guardianes, porque la lista de guardianes mantiene también las rutas heredadas. Un plugin de sandbox puede borrar esas rutas heredadas antes de arrancar una sandbox: el plugin OpenShell hace exactamente eso.
Los guardianes que se movieron con ellos
El traslado es la parte fácil. La difícil es cada sitio que daba por hecha la ubicación antigua. Este cambio actualizó:
- La lista de rutas de credenciales (
PROFILE_CREDENTIAL_PATHS): las comprobaciones de seguridad con alcance de perfil conocen los cuatro nombres nuevos. - La capa de seguridad de archivos (
agent/file_safety): denegaciones de lectura y de escritura, no solo de lectura. - La lista de denegaciones de entrega de medios del gateway: para que un archivo de credenciales no pueda salir por un adjunto o una ruta de medios.
- La API de archivos del panel: los cuatro nombres son nombres ocultos y nunca aparecen en el explorador de archivos.
- Las copias de seguridad: omiten los cuatro nombres de primer nivel. Esto importa: una caché de credenciales no debería viajar en una copia de seguridad a otra máquina.
- El
.gitignorede la instalación plana: los cuatro nombres de primer nivel se ignoran, para que no se confirmen por accidente.
La decisión de diseño merece copiarse: no «añadir un caso especial», sino «hacer que una lista sea la única fuente de verdad». Añade otro almacén de credenciales más adelante y todos los guardianes lo recogerán automáticamente, lo cual es bastante más fiable que escribirlo en seis sitios.
Qué verás tras actualizar
~/.hermes/bws_cache.json,~/.hermes/op_cache.jsony compañía aparecen en la raíz del home (o bajo~/.hermes/profiles/<name>/si usas varios perfiles).- En el primer uso de Bitwarden o 1Password, las copias antiguas bajo
cache/desaparecen y la caché nueva aparece en la ruta nueva. - Las copias de seguridad ya no contienen estos archivos.
- La lista de archivos del panel no los muestra.
- Dentro de una sandbox, renombrar un subdirectorio ya no los mueve.
Si tienes un script que lee cache/bws_cache.json directamente, necesita la ruta nueva. Es un cambio incompatible, pero lo que rompe es precisamente la ubicación insegura.
¿Puedes usarlo ya?
El cambio llegó a main el 2026-10-31 (PR #146715, etiquetado type/security) y ya está en la v0.22.6 (2026-11-02): tras actualizar con el instalador o hermes update, las cachés pasan a la raíz del perfil.
Hasta que salga, si ejecutas código no confiable a través de la sandbox MXC —sobre todo en Windows con el plugin OpenShell—, no trates «añadí la ruta de la caché a deniedPaths» como una garantía de seguridad. El truco del renombrado descrito arriba la derrota. La respuesta robusta es no mantener credenciales de alto valor y código de sandbox no confiable en el mismo perfil.
Para el contexto más amplio sobre cómo gestiona Hermes las credenciales, hay dos piezas complementarias que resultan útiles: cómo se cifran las claves y dónde queda el compromiso del llavero, y cómo se redactan los secretos de camino a los logs y los mensajes. Aquellas cubren cómo se guardan las claves y cómo se filtran; esta cubre dónde vive el archivo que las contiene.