Tu agente escribió en tu crontab real a las 2 de la madrugada: aislar HOME y cron


Le das a un agente una tarea de eval de lo más corriente: «escribe un backup.sh y prográmalo para que se ejecute cada noche a las 2 de la madrugada». Al día siguiente miras la máquina y crontab -l lista tres trabajos que tú nunca escribiste, ~/.hermes/scripts/ contiene dos scripts, y los directorios a los que apuntan se borraron hace mucho — así que cada noche a las 2 fallan, en silencio, durante meses.

Lo que se rompió no fue la competencia del agente, sino una lectura demasiado estrecha de la palabra «sandbox». El harness aislaba HERMES_HOME, daba por hecho que el estado propio de Hermes estaba contenido y dejaba los procesos hijo de la terminal corriendo como el usuario del operador — escribiendo en el ~ real, programando en el crontab real y tocando el gestor de usuario real de systemd.

Las tres salidas que se escaparon

Todo lo que Hermes posee vive bajo HERMES_HOME: la base de datos de sesiones, la configuración, las skills, la memoria. Apuntar HERMES_HOME a un directorio temporal sí garantiza de verdad que «todo quede limpio al terminar la ejecución».

Los comandos que se ejecutan en la terminal son otra historia. Cuando el agente ejecuta bash -c '...', ese hijo hereda el entorno del host:

Salida Consecuencia
HOME apuntando al home real write_file("~/.hermes/scripts/x.sh"), añadir a ~/.bashrc, tocar ~/.ssh/... — todo acaba en la máquina real
El comando crontab del sistema crontab - escribe la programación real del operador
DBUS_SESSION_BUS_ADDRESS / XDG_RUNTIME_DIR systemctl --user alcanza el gestor de usuario real y registra servicios de usuario reales

Junta todo eso y obtienes el resultado absurdo: una ejecución de eval ya terminada que sigue lanzando trabajos programados para siempre. El PR del fix (#132314) documenta la escena en una máquina de desarrollo: tres trabajos cron nocturnos, dos scripts en el ~/.hermes/scripts/ real y 24 directorios de ejecución /tmp/ab-* vacíos.

El producto ya tenía el contrato: TERMINAL_HOME_MODE

Lo interesante del fix es que no inventa una capa de aislamiento: usa una que Hermes ya distribuye. TERMINAL_HOME_MODE decide qué HOME reciben los hijos de la terminal:

Valor Significado
auto (predeterminado) HOME real en hosts; {HERMES_HOME}/home dentro de contenedores
real Siempre el HOME real
profile Fija siempre HOME en {HERMES_HOME}/home

(isolated y profile_home se aceptan como alias y se normalizan a profile.)

Merece la pena leer el código solo por la precedencia: get_subprocess_home() implementa esas decisiones, y apply_subprocess_home_env() es donde ocurre el trabajo — reescribe HOME al home del profile, registra el home real en HERMES_REAL_HOME y reapunta las variables del directorio temporal a un directorio de scratch dentro de HERMES_HOME. Una sola variable de entorno, y tanto la ruta de «escribir un archivo» como la de «escribir un archivo temporal» acaban dentro del profile.

Hay un detalle complementario del lado del guardián de escritura: al decidir si una ruta es un home protegido, el guardián toma la unión del home del proceso, el home real, el home del subproceso y el home del profile. De lo contrario tendrías el caso perverso en que HOME está fijado al profile pero una escritura con ruta absoluta en el home real queda sin proteger.

Qué hace realmente el sandbox de eval

Con el contrato entendido, las aproximadamente 40 líneas de código del sandbox se leen sin esfuerzo. Antes de que corra cualquier código del agente, hace cuatro cosas:

# evals/core_tool_deferral/sandbox.py (extracto)
os.environ["HOME"] = home                      # 1. el propio ~ del proceso apunta al home del profile
os.environ["TERMINAL_HOME_MODE"] = "profile"   # 2. los hijos de la terminal lo siguen
os.environ["PATH"] = bin_dir + os.pathsep + os.environ.get("PATH", "")  # 3. el shim de crontab primero
for var in ("DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR"):
    os.environ.pop(var, None)                  # 4. desconecta el bus de usuario de systemd

El crontab del paso 3 es un script de shell de unas 10 líneas que implementa la superficie real de la CLI — -l, -r, -e, leyendo de stdin o de un archivo — mientras lo guarda todo en un archivo de texto plano dentro del directorio de la ejecución. El agente recibe exactamente la misma retroalimentación (crontab -l muestra los trabajos que programó) y la programación real del operador no se toca ni un byte. Esa es la idea más copiable de todo esto: no aislar negándose, sino aislar sustituyendo por algo que se comporta igual.

El paso 1 parece redundante y no lo es. Reescribir el HOME del hijo de la terminal no basta, porque la herramienta write_file que recibe el modelo expande ~ dentro del proceso — así que el HOME propio del proceso también tiene que moverse, o write_file("~/.hermes/scripts/x.sh") seguirá aterrizando en la máquina real.

En cuanto a esos 24 directorios /tmp/ab-*: no hizo falta ningún cambio de código. Hermes ahora exporta TMPDIR apuntando a su propio directorio de scratch para cada proceso, así que un simple mkdtemp() ya aterriza donde debe; los restos son anteriores a ese cambio.

Qué significa esto para tu instalación

Si ejecutas un solo agente en tu propio portátil, lo más probable es que nunca toques TERMINAL_HOME_MODE — que auto mantenga el HOME real en los hosts es justo lo que quieres, porque tus scripts están pensados para escribir en tu propio directorio home.

Tres escenarios necesitan de verdad este ajuste:

  1. Evals y ejecuciones por lotes. Le pides al agente que escriba un script y lo programe, y docenas de ejecuciones no pueden compartir el estado de la máquina. TERMINAL_HOME_MODE=profile (más un shim de crontab y la desconexión del bus de usuario) es el suelo, no el techo.
  2. Máquinas multiinquilino o compartidas. Que el agente de otro profile escriba en tu directorio home no es hipotético. Más allá de esta variable, pon la terminal en un backend aislado (Docker, SSH) para que el agente no pueda alcanzar en absoluto el sistema de archivos del operador.
  3. Trabajos cron desatendidos. Un trabajo lanzado por el gateway que toca crontab también escribe la programación real. Contén primero su HOME y su PATH.

Una autocomprobación práctica: dale al agente una tarea de solo lectura — «lista los trabajos programados actuales» — y observa si lee tu tabla real o un directorio de ejecución vacío. Eso te dirá más sobre tu sandbox que cualquier documento.

Cuándo puedes conseguirlo

El fix del sandbox se fusionó en la rama main el 3 de octubre de 2026. La última release es v0.21.5 (tag v2026.9.24), así que el cambio en sí está solo en main — mientras que el contrato TERMINAL_HOME_MODE ya existe en las versiones publicadas y se puede usar hoy mismo.

Para ir más lejos, el artículo sobre el sandbox y la entrega de medios cubre el otro lado de la frontera; el artículo sobre la pérdida de datos por un árbol de trabajo sucio es la trampa en la que caes cuando el agente toca tu repositorio; y la guía del plugin de backend de terminal muestra cómo mover la terminal por completo a otro backend.

La versión corta

La lección se generaliza mucho más allá de este fix: cuenta las salidas, no los directorios. HERMES_HOME es el estado propio de Hermes; el directorio home, el crontab, el bus de usuario y el directorio temporal son cuatro salidas del sistema operativo, y con que se te escape una ya hay filtración. Si escribes harnesses de eval o montas configuraciones multiinquilino, TERMINAL_HOME_MODE=profile más un shim de crontab compatible en comportamiento es el par de cambios de mayor valor que puedes hacer.