Tu exportación de perfil estaba enviando tus credenciales de la nube

Te pasas una mañana ajustando un perfil y luego se lo entregas a un compañero de equipo: hermes profile export work -o work.tar.gz, con una nota que dice “descomprime esto y tienes mi configuración”. Lo descomprime y efectivamente tiene tu configuración — además de tus credenciales de registro privado. El tar.gz contiene skills/, config.yaml, state.db y un .docker/config.json en el que nunca pensaste.
Eso no era un agujero hipotético. hermes profile export excluía algunos archivos de credenciales pero se saltaba por completo los directorios de credenciales a nivel de sistema operativo. Dos PRs fusionados el 3 y el 4 de octubre (#132201 y #132654) lo cerraron. Esto es lo que se filtró, por qué, y qué deberías revisar hoy.
Etapa uno: .ssh, .aws, .gnupg, .kube
Una corrección anterior (#130525) ya mantenía los secretos propios de Hermes — auth.json, .env, tokens OAuth — fuera de las exportaciones. Pero PROFILE_CREDENTIAL_PATHS solo cubría el subárbol home/ del subproceso; los almacenes del sistema operativo simplemente no estaban en la lista.
#132201 añadió el primer lote: .ssh, .aws, .gnupg, .kube y el .envrc de direnv. También corrigió dos problemas vecinos:
- Anidamiento. Un directorio de skill copiado de un home real puede llevar
skills/some-skill/.ssh/id_rsa. El antiguo filtro de exportación solo coincidía con nombres de nivel superior, así que un.sshanidado se enviaba igual. Tras la corrección, esos almacenes se descartan a cualquier profundidad. - Instalaciones de distribución. Cuando instalas una distribución de perfil, esas rutas se marcan como propiedad del usuario (
profile_path_is_private), así que un paquete ya no puede plantarlas ni sobrescribirlas en tu máquina.
Etapa dos: .docker, .azure, .config/gh, .config/gcloud
Un día después, #132654 detectó que cuatro directorios hermanos seguían sin cubrirse — y eran justo los directorios que agent/file_safety.py ya denegaba la escritura. Hermes se negaba a escribirlos y luego los exportaba tan contento:
| Directorio | Qué suele vivir ahí |
|---|---|
.docker/config.json |
Autenticación de registro para imágenes privadas |
.azure/accessTokens.json |
Tokens de acceso de Azure CLI |
.config/gh/hosts.yml |
Tu token de GitHub CLI |
.config/gcloud/ |
ADC de Google Cloud y credentials.db |
La referencia de contexto @file lo empeoraba: escribir @file:~/.config/gcloud/application_default_credentials.json en un mensaje expandía el token al contexto. Y el gateway hace pasar texto remoto no confiable por la expansión de @file, así que era la misma clase de fuga.
Por qué ocurrió: tres listas copiadas a mano
La causa raíz no fue un directorio olvidado. La misma lista se había copiado a mano en tres sitios, y las copias se habían desviado:
- la lista de denegación de escritura en
agent/file_safety.py; - el filtro de exportación en
hermes_cli/profiles.py; - la lista de rechazo de
@fileenagent/context_references.py.
La corrección le da a la lista una única fuente. agent/file_safety.py ahora contiene:
HOME_CREDENTIAL_DIRS = (".ssh", ".aws", ".gnupg", ".kube",
".docker", ".azure", ".config/gh", ".config/gcloud")
build_write_denied_prefixes deriva de ella, y hermes_cli/profiles.py desempaqueta la misma tupla en _OS_CREDENTIAL_STORES, así que las listas de exportación (PROFILE_CREDENTIAL_PATHS, _EXPORT_CREDENTIAL_FILES) ya no pueden desviarse de la lista de denegación de escritura. agent/context_references.py también la importa, y así fue como @file ganó .config/gcloud.
El filtro de exportación también cambió su forma de coincidir — por componentes finales de la ruta en lugar de nombres de nivel superior:
def _export_credential_entries(directory: str, contents: list) -> set:
"""Entries of *directory* that are an _EXPORT_CREDENTIAL_FILES store: matched on trailing path
components, so ``.config/gh`` drops at any depth while the rest of ``.config`` ships."""
Así, .config/gh y .config/gcloud se descartan estén donde estén, mientras que contenido no relacionado como .config/other, .config/ghost o .config/gh.bak sigue exportándose con normalidad.
¿Ya exportaste uno? Asume que está expuesto
Esta corrección solo protege las exportaciones hechas después de ella. Si ya has compartido un archivo de perfil — con un compañero, un servidor de archivos o un repositorio git — la respuesta correcta es rotar, no borrar el archivo:
- Mira primero qué hay realmente dentro del archivo (un tar.gz se lista sin descomprimir):
tar -tzf work.tar.gz | grep -E '(^|/)\.(ssh|aws|gnupg|kube|docker|azure|envrc)/|\.config/(gh|gcloud)/'
- Todo lo que coincida está quemado: reemplaza las claves SSH, rota las credenciales de AWS / Azure / gcloud, revoca y reemite el token de GitHub, cambia la contraseña del registro privado.
- Exporta de nuevo, confirma que está limpio (el mismo grep no devuelve nada) y comparte eso.
La versión corta
hermes profile exportproduce un tar.gz y ahora excluye los archivos de credenciales propios de Hermes más ocho directorios de credenciales a nivel de home;- la brecha existía porque tres listas copiadas a mano se separaron; la corrección hace que una sola lista sea la autoritativa y coincide por componentes finales de la ruta;
- la expansión de
@filey las instalaciones de distribución comparten esa lista, así que las tres se apretaron juntas; - la corrección mira hacia adelante — rota todo lo que ya hayas enviado.
Para el panorama más amplio de aislamiento, consulta las tres vías de salida para la redacción de secretos y renombrar un perfil sin perder su identidad; la superficie completa del subcomando está en la referencia de hermes profile.