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:

  1. La sandbox dice que home/cache/bws_cache.json no debe leerse.
  2. El agente tiene permiso de escritura sobre home/cache/, así que renombra el directorio a home/cache-old/.
  3. El archivo se mueve con su directorio: ahora es home/cache-old/bws_cache.json.
  4. 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 .gitignore de 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.json y 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.