La clave de API que imprimiste una vez no debería acabar en un log de chat

Son las diez de la noche y estás depurando un 401. Haces cat .env en el terminal para ver si la clave quedó bien puesta, y Hermes lee esa salida dentro de la sesión. Veinte minutos después, el job programado que montaste antes publica «comprobación de config de hoy» en el grupo de Telegram de tu equipo, con tu clave de API completa en mitad del mensaje. Lo único que hiciste fue mirar tu propio archivo de config.
Esa es la verdadera forma de una filtración de credenciales: nadie entra a robar nada. El mismo texto simplemente viaja de la salida de una tool al historial de la sesión y de ahí a un mensaje saliente, y en alguna de esas paradas nadie tiene la responsabilidad de enmascararlo. Los dos arreglos fusionados el 18 de septiembre de 2026 tapan una parada cada uno: el PR #114839 repara las reglas de enmascarado de la salida de las tools, y el PR #114789 repara la política de redacción en las líneas de entrega de cron. Esto es lo que hace cada uno y, lo que importa igual o más, lo que todavía no protegen.
Primera parada: las reglas de enmascarado se dejaban fuera dos formas reales de credencial
Hermes tiene un redactor (agent/redact.py) por el que pasan la salida del terminal, las lecturas de archivos y todo lo que va detrás de ellas: el almacenamiento de sesiones, session_search. Reconoce a los proveedores por una tabla de prefijos: ghp_, xoxb-, AKIA, hf_, pplx- y demás. Está activado por defecto y lo gobierna security.redact_secrets (desactivarlo registra un aviso al arrancar).
El hueco eran dos formas que nunca llegaron a entrar en la tabla:
- Claves
sk-con puntos del stack de Alibaba. Claves comosk-sp-…ysk-ws-…llevan puntos en el cuerpo, y la antigua clase de caracteres no incluía el., así que la coincidencia se detenía en el primer punto: la cabecera quedaba enmascarada y la cola se quedaba en texto claro. Una clave que parece redactada mientras sobrevive la mitad de ella es peor que una que claramente nunca se tocó. - Las claves
id.secretde Zhipu, sin prefijo. Al no llevar un prefijo del estilosk-/ghp_, la tabla de prefijos no podía detectarlas nunca, y la credencial entera viajaba tal cual.
El arreglo hace dos cosas. La entrada de sk- ahora acepta un cuerpo delimitado por puntos, así que sk-sp-… y sk-ws-… coinciden por completo, y un punto de fin de frase después de la clave nunca se consume. Para Zhipu se añadió una regex deliberadamente con forma de proveedor: un id de 32 caracteres en hex minúscula, un punto y un segmento de credencial de al menos 16 alfanuméricos. El motivo de no tirar por una regla genérica de «token con puntos de alta entropía» son los falsos positivos: nombres de archivo con hash de contenido como <sha>.bundle o <md5>.sqlite3 no deben coincidir. Los tests llevan controles negativos precisamente para eso: un sha desnudo de 40 hex, un fragmento corto de sk- y un id truncado de 31 caracteres tienen que salir byte a byte idénticos.
Medido contra una prueba real:
| Entrada | Antes | Después |
|---|---|---|
api_key: sk-sp-...7890.abcdefgh1234567890_XYZ-0987654321 |
la cola se filtraba en texto claro | sk-sp-...4321 |
api_key: 50aaedFAKE1234567890abcdef1234567890abc.ZpSh99 |
tal cual | 50aaed...Sh99 |
el mismo archivo a través de file_read |
«redacted:sk-…».abcdefg… la cola sobrevivía |
«redacted:sk-…» / «redacted-secret» |
Esa tercera fila merece su propio párrafo: el camino de «contenido de archivo» no enmascara, sustituye por un centinela. Detrás hay un incidente real (issue #35519). Un agente leyó una clave que había quedado enmascarada con una forma que parecía una credencial auténtica pero truncada, la volvió a escribir en config.yaml y destruyó para siempre la credencial guardada, dejándola en un valor muerto de 13 caracteres: 401 a partir de ahí. El centinela («redacted-secret») es sintácticamente imposible como token usable, así que nunca podrá confundirse con uno ni volver a escribirse. El rendimiento también aguantó: entradas patológicas —un cuerpo de 20.000 caracteres, 10.000 puntos, 2.000 secuencias hex— se redactan en menos de 3 milisegundos cada una.
Segunda parada: cada línea de entrega de cron ahora redacta, por fuerza
La salida de las tools era un camino. El más fácil de olvidar es que la salida de un job de cron sale de la máquina por diseño. En la implementación anterior, el stdout y el stderr de un job de script se limpiaban en el punto donde se capturaban, pero el texto de respuesta de un job de LLM llegaba a _deliver_result sin pasar por el escáner, y cada línea por debajo enviaba la cadena en crudo.
El PR #114789 coloca la redacción en el único punto de estrangulamiento de la entrega y añade una red de seguridad que falla en cerrado (fail closed):
cron/scheduler_delivery.pygana_redact_cron_payload(), que llama aredact_sensitive_text(text, force=True).- Qué significa
force=True: esto es una frontera de salida.security.redact_secretsgobierna los logs en tu propia máquina; esto gobierna lo que sale de ella. Así que el ajuste del usuario no puede desactivarlo aquí. - Fail closed: si el propio redactor lanza una excepción, todo el payload se sustituye por
[REDACTED - redaction failed]en lugar de dejarlo pasar. Es la misma postura que ya se usa donde se captura el stdout del shell. - Cubre cuatro líneas: el mensaje a la plataforma (el punto de estrangulamiento
cleaned_delivery_content), el texto del espejo de sesión (que se deriva delcontenten crudo y por eso no heredaba el chokepoint), el nombre del job, que llega a los prefijos del espejo y a los títulos de hilo, y el turno de chat del bot que se pasa a otro profile, dondecontentse reasigna para que el registro diferido duradero y su replay también lleven la copia ya limpiada. - La línea de aviso de fallo no necesitaba un cambio aparte: sale por el mismo punto de estrangulamiento
_deliver_resulty queda cubierta por construcción.
Merece la pena conocer una excepción deliberada: redact_url_credentials no está activado para cron. Estos jobs entregan legítimamente enlaces mágicos y URLs prefirmadas a tu propio chat, y quitar de cuajo las URLs con credenciales rompería la funcionalidad. Es el mismo criterio que agent/redact.py ya documenta para las fronteras de salida del chat.
Lo que esto sigue sin cubrir
Ambos arreglos son reales, pero atienden la salida del propio Hermes. Hay tres cosas que siguen siendo tu responsabilidad:
- No pegues credenciales en la conversación. El texto que pegas tú también acaba en el historial de la sesión y en la base de datos. El redactor hace lo que puede con las formas conocidas, pero «formas conocidas» es siempre una lista incompleta, y una clave de terceros puede no tener ninguna forma reconocible. Para comprobar un valor de config, usa
hermes config get <key>y confirma presencia, longitud o los últimos cuatro caracteres, nunca el valor en sí. - No hagas echo de archivos sensibles desde scripts de cron. La línea de entrega redacta, pero los logs del propio script, la salida de tu CI y tu historial de shell todavía pueden conservar el original. Imprime el veredicto («KEY present» / «KEY missing»), nunca el valor.
- No desactives la redacción para expresar frustración.
security.redact_secrets: falsesolo afecta al camino de los logs locales y registra un aviso al arrancar; no puede llegar a la línea de cron, que va forzada. Si estás depurando el propio redactor, vuelve a activarlo después.
Para el panorama más amplio sobre cómo guardar y rotar credenciales, mira la guía de cifrado del keychain. Si tu línea de notificación es una plataforma de chat, la guía de aprobaciones desatendidas cubre la otra mitad de ese control y, para los propios jobs, empieza por la guía de diagnóstico de cron y la referencia de hermes secrets.
Estado de la release
Ambos PR se fusionaron en main el 18 de septiembre de 2026, y el tag más reciente es la v0.21.3 (nombre del tag v2026.9.14, publicado el 14 de septiembre), así que ninguno está todavía en una release etiquetada. Una instalación git los recoge en la próxima actualización:
hermes --version
hermes update
Los usuarios de imagen Docker y de la nube los reciben con el tag siguiente.
El patrón que merece la pena conservar
Vistos juntos, la regla es sencilla: la redacción es una lista que tiene que mantenerse al día, va en el único punto de salida y necesita una red de seguridad que falle en cerrado. Añadir un nombre a la tabla de prefijos no basta si el cuerpo puede contener puntos; limpiar unas cuantas líneas no basta cuando el espejo de sesión y el nombre del job sacan el mismo texto; y un redactor que ante un error recurre a enviar el texto original no protege absolutamente nada. Esas tres costuras son las que se han arreglado.