Caches de segredos na raiz do perfil: o buraco no sandbox que um rename de pasta podia abrir


Você configurou o Hermes com o Bitwarden para que ele busque a senha do banco sozinho, em vez de você colar tudo a cada vez. Sendo cuidadoso, colocou tudo dentro de um sandbox e marcou cache/bws_cache.json como caminho negado — afinal, esse arquivo é o cache de chaves descriptografadas, em texto puro.

Aí descobre que a negação cai com um empurrão: renomeie a pasta cache/ para cache2/ e o arquivo bloqueado viaja com ela. Leia no caminho novo e a proteção não reconhece mais nada.

Isso não é distração de uma pessoa. É um descompasso de fronteira clássico: o sandbox nega por caminho, mas a pasta-mãe desse caminho está dentro do escopo de escrita do agente. Em 2026-10-31 o Hermes moveu os quatro arquivos de cache de credenciais para o topo da home do perfil, eliminando o bypass em vez de remendar em volta dele.

O problema não era “escondido raso demais” — era “escondido numa pasta que pode ser renomeada”

Vale entender isso direito, porque a lição sobrevive ao bug.

No Windows, o sandbox MXC (o que o plugin OpenShell usa) bloqueia arquivos por deniedPaths. Esse mecanismo pressupõe que o arquivo negado esteja diretamente na home concedida. Se ele mora dentro de um subdiretório, e o agente pode criar, escrever e renomear esse subdiretório, a negação é decorativa:

  1. O sandbox diz que home/cache/bws_cache.json não pode ser lido.
  2. O agente tem escrita em home/cache/, então renomeia o diretório para home/cache-old/.
  3. O arquivo vai junto com o diretório: agora é home/cache-old/bws_cache.json.
  4. Esse caminho não está na lista de negação. Leia. Pronto.

O pull request diz isso sem rodeios: o bypass se sustenta no MXC 1.0.0 e no 1.0.1, conforme medido. Ou seja, a promessa anterior — “este caminho é negado” — não sobrevive à condição “o arquivo negado mora num subdiretório com escrita liberada”, e os quatro caches de credenciais cumpriam essa condição.

A conclusão é seca: não dá para consertar isso com uma lista de negação maior. Os arquivos precisam parar de morar num subdiretório com escrita liberada.

Para onde foram os quatro arquivos

O que guarda Caminho antigo (dentro de um subdiretório com escrita) Caminho novo (topo da home do perfil)
Cache em texto puro do Bitwarden cache/bws_cache.json bws_cache.json
Cache criptografado do Bitwarden cache/bws_cache.enc.json bws_cache.enc.json
Cache do 1Password cache/op_cache.json op_cache.json
Registro do sidecar Photon (porta, token, pid) runtime/photon-sidecar.json photon-sidecar.json

Os nomes de arquivo não mudaram; só o nível de diretório mudou. Você não consegue mover esses arquivos renomeando uma pasta, porque eles não são arquivos dentro de uma pasta — são filhos diretos da própria home, e um sandbox que os nega nega exatamente aquele caminho.

Uma lacuna antiga foi fechada na mesma passada: o cache do 1Password não era coberto por nenhuma proteção antes disso. Então a mudança não é só um movimento; os quatro nomes agora vivem numa única lista compartilhada (SECRET_CACHE_FILES em hermes_cli/home_data_layout.py), e todo lugar que precisa saber “estes arquivos são credenciais” lê essa mesma lista.

As cópias antigas: apagadas sem serem lidas

A pergunta natural depois de atualizar é o que acontece com cache/bws_cache.json.

Ele é apagado no primeiro uso, sem ser lido. O motivo é simples: esse caminho fica dentro de um diretório em que o agente pode escrever, então o conteúdo já não é confiável. Lê-lo seria convidar de volta dados possivelmente adulterados. A remoção é idempotente, e o próximo uso do Bitwarden ou do 1Password simplesmente busca de novo.

O custo é mínimo porque os quatro caches são regeneráveis:

  • Caches do Bitwarden e do 1Password: no máximo uma busca extra depois da atualização.
  • O registro do sidecar Photon: reconstruído na próxima vez que o gateway reiniciar.

Existe um caso de borda honesto. Um perfil que nunca mais toca na fonte Bitwarden ou 1Password mantém o arquivo antigo em cache/, sem ser lido. Ele continua coberto por todas as proteções, porque a lista mantém os caminhos legados também. Um plugin de sandbox pode apagar esses caminhos legados antes de subir um sandbox — o plugin OpenShell faz exatamente isso.

As proteções que foram junto

Mover é a parte fácil. A parte difícil é cada lugar que assumia o caminho antigo. Esta mudança atualizou:

  • A lista de caminhos de credenciais (PROFILE_CREDENTIAL_PATHS): as checagens de segurança por perfil conhecem os quatro nomes novos.
  • A camada de segurança de arquivos (agent/file_safety): negação de leitura e de escrita, não só de leitura.
  • A lista de negação de entrega de mídia do gateway: para que um arquivo de credencial não possa sair por um anexo ou por um caminho de mídia.
  • A Files API do dashboard: os quatro nomes são nomes ocultos, nunca listados no navegador de arquivos.
  • Backups: os backups pulam os quatro nomes de topo. Esse importa — um cache de credenciais não deve pegar carona num backup para outra máquina.
  • O .gitignore da instalação flat: os quatro nomes de topo são ignorados, então não podem ser commitados por acidente.

A escolha de design aqui merece ser copiada: não “adicione um caso especial”, mas “faça de uma lista a única fonte de verdade”. Adicione outro cofre de credenciais depois e todas as proteções o pegam automaticamente, o que é bem mais confiável do que repetir a lista em seis lugares.

O que você vai ver depois de atualizar

  • ~/.hermes/bws_cache.json, ~/.hermes/op_cache.json e companhia aparecem no topo da home (ou em ~/.hermes/profiles/<nome>/ quando você roda vários perfis).
  • No primeiro uso do Bitwarden ou do 1Password, as cópias antigas em cache/ somem e o cache novo aparece no caminho novo.
  • Os backups não contêm mais esses arquivos.
  • A lista de arquivos do dashboard não os mostra.
  • Dentro de um sandbox, renomear um subdiretório não os move mais.

Se você tem um script que lê cache/bws_cache.json direto, ele precisa do caminho novo. É uma breaking change — mas o que ela quebra é justamente o local inseguro.

Já dá para usar hoje

A mudança entrou na main em 2026-10-31 (PR #146715, com a etiqueta type/security) e já está na v0.22.6 (2026-11-02): depois de atualizar pelo instalador ou hermes update, os caches vão para a raiz do perfil.

Enquanto não sair, se você roda código não confiável pelo sandbox MXC — especialmente no Windows com o plugin OpenShell —, não trate “adicionei o caminho do cache ao deniedPaths” como garantia de segurança. O truque do rename acima derruba isso. A resposta robusta é não manter credenciais valiosas e código não confiável de sandbox no mesmo perfil.

Para o contexto mais amplo de como o Hermes lida com credenciais, dois textos companheiros ajudam: como as chaves são criptografadas e onde fica o trade-off do keychain, e como segredos são redigidos no caminho até logs e mensagens. Aqueles cobrem como as chaves são guardadas e como vazam; este cobre onde mora o arquivo que as contém.