Quer que o Hermes Edite Seu SSH Config? Agora Ele Pergunta Primeiro


Você pede ao Hermes para adicionar um novo servidor ao seu config SSH — um alias de host, um bastion com ProxyJump, para que ssh work conecte em uma linha a partir de agora. write_file bate a porta na sua cara: “Escrita negada: ~/.ssh/config é um arquivo de sistema/credencial protegido.” Então você recorre à ferramenta terminal e usa printf para gravar as mesmas linhas no mesmo arquivo — e ela apenas exibe uma caixa de confirmação, você clica em aprovar e pronto. Mesmo arquivo, duas ferramentas, dois conjuntos de regras contraditórios — qual deles está certo? O PR #84663, mesclado em 12 de agosto, resolve a questão: o config do cliente SSH deixou de ser uma zona de “nem pense nisso” — ele foi movido para um approval gate que pergunta antes de agir.

A causa raiz: um arquivo, duas regras conflitantes

A contradição vinha de duas camadas de segurança independentes:

  • write_file / patch checam a lista de negação categórica em agent/file_safety.py. ~/.ssh/config costumava acionar tanto a negação por caminho exato quanto a negação por prefixo ~/.ssh/, então era rejeitado de imediato, sem margem para negociação.
  • terminal passa pela lógica de aprovação de comandos perigosos em tools/approval.py. Escrever em ~/.ssh apenas era sinalizado como “precisa de aprovação” — aprove e a escrita passa.

Então o mesmo ~/.ssh/config era um beco sem saída pelas ferramentas de arquivo e uma confirmação de um clique pelo terminal. Essa inconsistência confundia os usuários — e o próprio agente reportava “escrita falhou” primeiro e “sucesso” depois, pelo outro caminho, deixando um rastro de logs contraditórios.

A correção: ~/.ssh/config rebaixado de negação categórica para approval gate

O raciocínio do PR #84663: o config do cliente SSH não é material de credencial. Ele não guarda bytes de chave privada, e editá-lo (aliases de host, ProxyJump, alvos do VS Code Remote-SSH) é uma tarefa rotineira iniciada pelo usuário — uma recusa seca é errada. Mas ele pode carregar diretivas ProxyCommand / Match exec que executam comandos, então uma escrita livre e silenciosa também é errada. Aprovação é a política correta — alinhada ao que a ferramenta terminal já fazia para escritas em ~/.ssh.

Verificado contra origin/main:

  • agent/file_safety.py: ~/.ssh/config removido da negação plana de credenciais; os novos build_write_approval_paths() + is_write_approval_required() desviam caminhos com approval gate para fora da negação por prefixo ~/.ssh/ em _classify_write_denial.
  • Chaves privadas e arquivos de autenticação continuam com negação categórica: id_rsa, id_ed25519, authorized_keys e tudo mais sob ~/.ssh/ — sem exceções.
  • tools/file_tools.py: write_file e patch agora roteiam escritas no config SSH pelo _run_approval_gate compartilhado, logo após o gate de instruções protegidas.
  • Chamadas não interativas falham de forma segura (fail-closed): a ponte de arquivos ACP (agent/copilot_acp_client.py) rejeita caminhos que exigem aprovação de imediato; o seletor de caminho de saída do TTS (tools/tts_tool.py) também os recusa.

O modelo de camadas de segurança de arquivos do Hermes

Esta mudança também expõe o modelo completo de segurança de escrita, com três camadas que valem a pena lembrar:

Camada 1: negação categórica — nem pense nisso. Credenciais e arquivos críticos do sistema não podem ser escritos por nenhuma ferramenta em nenhum modo, nem mesmo com yolo: ~/.ssh/authorized_keys, id_rsa, id_ed25519, .env, .anthropic_oauth.json, .netrc, .pgpass, .npmrc, .pypirc, .git-credentials, /etc/sudoers, /etc/passwd, /etc/shadow, além de prefixos de diretório como ~/.ssh/, ~/.aws/, ~/.gnupg/, ~/.kube/, ~/.docker/, ~/.config/gh/. Este é o piso que nunca se move.

Camada 2: approval gate — pergunte primeiro. Atualmente com um único membro: ~/.ssh/config. Escrevível, mas apenas através de um prompt de aprovação humano, porque pode contrabandear diretivas que executam comandos. O gate oferece três escopos de persistência: once (só desta vez), session (lembrar nesta sessão), always (lembrar para sempre).

Camada 3: escrita livre — todo o resto. Arquivos de trabalho comuns e código de projeto, o agente pode escrever livremente.

Como o approval gate se comporta na prática

Lendo o gate compartilhado (_run_approval_gate em tools/approval.py), a ordem de decisão é:

  1. --yolo passa primeiro: o modo yolo (em nível de processo, HERMES_YOLO_MODE, ou de sessão) atravessa direto — mas as negações categóricas da camada 1 são verificadas antes do gate, então nem o yolo consegue tocá-las;
  2. Cache de sessão como atalho: uma escrita aprovada anteriormente (escopo session/always) passa em silêncio;
  3. Ramificação interativa / gateway / cron: sessões interativas recebem um prompt; sessões cron seguem approvals.cron_mode (padrão: negar);
  4. Fail-closed sem canal humano: sem terminal interativo nem sessão gateway (ex.: scripts em segundo plano, chamadas ACP) → BLOQUEADA, sem discussão. A negação inclusive diz ao agente para não tentar de novo via terminal ou execute_code, a menos que o usuário consinta explicitamente.

Conclusão

Mover ~/.ssh/config da negação categórica para o approval gate é, no fundo, devolver a decisão de “a ferramenta decide” para “você decide”: edições rotineiras não são mais bloqueadas de imediato, enquanto escritas que carregam comandos sempre passam por um gate humano. Para saber mais sobre a maquinaria de aprovação do Hermes, veja nosso guia de Smart Approvals de três gates e como a fadiga de aprovações é resolvida; os modos --yolo e as linhas vermelhas estão cobertos em este post; os comandos de configuração vivem na página de referência hermes-config.