Seu agente escreveu na sua crontab de verdade às 2 da manhã: isolando HOME e cron


Você entrega a um agente uma tarefa de eval perfeitamente comum: “escreva um backup.sh e agende-o para rodar toda noite às 2 da manhã”. No dia seguinte, você olha para a máquina e crontab -l lista três tarefas que você nunca escreveu, ~/.hermes/scripts/ guarda dois scripts, e os diretórios para os quais eles apontam foram excluídos há muito tempo — então toda noite às 2 da manhã eles falham, em silêncio, durante meses.

O que quebrou não foi a competência do agente, mas uma leitura estreita demais da palavra “sandbox”. O harness isolava HERMES_HOME, presumia que o estado próprio do Hermes estava contido e deixava os processos filhos do terminal rodando como o usuário do operador — gravando no ~ real, agendando na crontab real e mexendo no gerenciador de usuário real do systemd.

As três saídas que vazaram

Tudo o que o Hermes possui vive sob HERMES_HOME: o banco de dados de sessões, a configuração, as skills, a memória. Apontar HERMES_HOME para um diretório temporário realmente garante que “tudo fique limpo quando a execução termina”.

Comandos executados no terminal são outra história. Quando o agente roda bash -c '...', esse filho herda o ambiente do host:

Saída Consequência
HOME apontando para o home real write_file("~/.hermes/scripts/x.sh"), anexar a ~/.bashrc, mexer em ~/.ssh/... — tudo atinge a máquina real
O comando crontab do sistema crontab - grava o agendamento real do operador
DBUS_SESSION_BUS_ADDRESS / XDG_RUNTIME_DIR systemctl --user alcança o gerenciador de usuário real e registra serviços de usuário reais

Junte tudo isso e você tem o resultado absurdo: uma execução de eval concluída que continua disparando tarefas agendadas para sempre. O PR do fix (#132314) documenta a cena em uma máquina de desenvolvimento: três tarefas cron noturnas, dois scripts no ~/.hermes/scripts/ real e 24 diretórios de execução /tmp/ab-* vazios.

O produto já tinha o contrato: TERMINAL_HOME_MODE

O interessante do fix é que ele não inventa uma camada de isolamento — ele usa uma que o Hermes já entrega. TERMINAL_HOME_MODE decide qual HOME os filhos do terminal recebem:

Valor Significado
auto (padrão) HOME real em hosts; {HERMES_HOME}/home dentro de contêineres
real Sempre o HOME real
profile Fixa sempre HOME em {HERMES_HOME}/home

(isolated e profile_home são aceitos como apelidos e normalizados para profile.)

Vale a pena ler o código só pela precedência: get_subprocess_home() implementa essas decisões, e apply_subprocess_home_env() é onde o trabalho acontece — ele reescreve HOME para o home do profile, registra o home real em HERMES_REAL_HOME e redireciona as variáveis de diretório temporário para um diretório de scratch sob HERMES_HOME. Uma única variável de ambiente, e tanto o caminho de “gravar um arquivo” quanto o de “gravar um arquivo temporário” acabam dentro do profile.

Há um detalhe complementar do lado do write-guard: ao decidir se um caminho é um home protegido, o guard pega a união do home do processo, do home real, do home do subprocesso e do home do profile. Caso contrário, você teria o caso perverso em que HOME está fixado no profile, mas uma gravação por caminho absoluto no home real fica sem proteção.

O que o sandbox de eval realmente faz

Com o contrato entendido, as cerca de 40 linhas de código do sandbox leem-se sem esforço. Antes de qualquer código do agente rodar, ele faz quatro coisas:

# evals/core_tool_deferral/sandbox.py (trecho)
os.environ["HOME"] = home                      # 1. o próprio ~ do processo aponta para o home do profile
os.environ["TERMINAL_HOME_MODE"] = "profile"   # 2. os filhos do terminal seguem
os.environ["PATH"] = bin_dir + os.pathsep + os.environ.get("PATH", "")  # 3. o shim de crontab primeiro
for var in ("DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR"):
    os.environ.pop(var, None)                  # 4. desconecta o barramento de usuário do systemd

O crontab do passo 3 é um script de shell de umas 10 linhas que implementa a superfície real da CLI — -l, -r, -e, lendo de stdin ou de um arquivo — enquanto guarda tudo em um arquivo de texto simples dentro do diretório da execução. O agente recebe exatamente o mesmo retorno (crontab -l mostra as tarefas que ele agendou) e o agendamento real do operador não é tocado nem um byte. Essa é a ideia mais copiável aqui: não isolar negando; isolar substituindo por algo que se comporta igual.

O passo 1 parece redundante e não é. Reescrever o HOME do filho do terminal não basta, porque a ferramenta write_file entregue ao modelo expande ~ dentro do processo — então o próprio HOME do processo também precisa mudar, senão write_file("~/.hermes/scripts/x.sh") continua caindo na máquina real.

Quanto àqueles 24 diretórios /tmp/ab-*: nenhuma mudança de código foi necessária. O Hermes agora exporta TMPDIR apontando para seu próprio diretório de scratch em cada processo, então um simples mkdtemp() já cai onde deveria; as sobras são anteriores a essa mudança.

O que isso significa para a sua configuração

Se você roda um único agente no seu próprio notebook, provavelmente nunca vai mexer em TERMINAL_HOME_MODE — o auto manter o HOME real nos hosts é exatamente o que você quer, já que seus scripts devem gravar no seu próprio diretório home.

Três cenários realmente precisam dessa configuração:

  1. Evals e execuções em lote. Você pede ao agente que escreva um script e o agende, e dezenas de execuções não podem compartilhar o estado da máquina. TERMINAL_HOME_MODE=profile (mais um shim de crontab e a desconexão do barramento de usuário) é o piso, não o teto.
  2. Máquinas multilocatárias ou compartilhadas. Que o agente de outro profile grave no seu diretório home não é hipotético. Além dessa variável, coloque o terminal em um backend isolado (Docker, SSH) para que o agente não consiga alcançar de forma alguma o sistema de arquivos do operador.
  3. Tarefas cron não assistidas. Uma tarefa lançada pelo gateway que mexe em crontab grava também o agendamento real. Contenha primeiro o HOME e o PATH dela.

Um teste prático que você mesmo pode fazer: dê ao agente uma tarefa somente leitura — “liste as tarefas agendadas atuais” — e veja se ele lê a sua tabela real ou um diretório de execução vazio. Isso dirá mais sobre o seu sandbox do que qualquer documento.

Quando você vai ter isso

O fix do sandbox foi mesclado na branch main em 3 de outubro de 2026. A última release é a v0.21.5 (tag v2026.9.24), então a mudança em si está apenas na main — enquanto o contrato TERMINAL_HOME_MODE já existe nas versões lançadas e pode ser usado hoje.

Para ir além, o artigo sobre sandbox e entrega de mídia cobre o outro lado da fronteira; o post sobre a perda de dados por árvore de trabalho suja é a armadilha em que você cai quando o agente mexe no seu repositório; e o guia do plugin de backend de terminal mostra como mover o terminal por completo para outro backend.

A versão curta

A lição se generaliza muito além desse fix: conte as saídas, não os diretórios. HERMES_HOME é o estado próprio do Hermes; o diretório home, a crontab, o barramento de usuário e o diretório temporário são quatro saídas do sistema operacional, e deixar escapar uma já basta para vazar. Se você escreve harnesses de eval ou mantém configurações multilocatárias, TERMINAL_HOME_MODE=profile mais um shim de crontab compatível em comportamento é o par de mudanças de maior valor que você pode fazer.