A API key que você mostrou uma vez não deveria acabar no log de um chat


É dez da noite e você está depurando um 401. Você roda cat .env no terminal para ver se a chave foi definida corretamente, e o Hermes lê essa saída para dentro da sessão. Vinte minutos depois, o job agendado que você montou antes publica “a conferência de config de hoje” no grupo do Telegram do seu time — com a sua API key inteira no meio da mensagem. A única coisa que você fez foi olhar o seu próprio arquivo de config.

Essa é a verdadeira cara do vazamento de credenciais: ninguém invade nada nem rouba nada. O mesmo texto apenas viaja da saída de uma ferramenta para o histórico da sessão e daí para uma mensagem de saída — e em uma dessas paradas ninguém é responsável por mascará-lo. Duas correções mescladas em 18 de setembro de 2026 remendam uma parada cada uma: o PR #114839 conserta as regras de mascaramento da saída de ferramentas, o PR #114789 conserta a política de redação nas faixas de entrega do cron. Veja o que cada uma faz — e, o que é igualmente importante, o que elas ainda não protegem.

Parada um: as regras de mascaramento deixavam passar duas formas reais de credencial

O Hermes tem um redator (agent/redact.py) pelo qual passam a saída de terminal, as leituras de arquivo e tudo o que vem depois delas — armazenamento de sessão, session_search. Ele reconhece fornecedores por uma tabela de prefixos: ghp_, xoxb-, AKIA, hf_, pplx- e assim por diante. Está ligado por padrão, governado por security.redact_secrets (desligá-lo registra um aviso na inicialização).

A lacuna eram duas formas que nunca entraram na tabela:

  • Chaves sk- com pontos da stack da Alibaba. Chaves como sk-sp-… e sk-ws-… trazem pontos no corpo, e a classe de caracteres antiga não incluía ., então a correspondência parava no primeiro ponto: o começo era mascarado e a cauda permanecia em texto claro. Uma chave que parece redigida enquanto metade dela sobrevive é pior do que uma que claramente nunca foi tocada.
  • As chaves id.secret sem prefixo da Zhipu. Sem um prefixo no estilo sk-/ghp_, a tabela de prefixos nunca conseguia casar com elas, e a credencial inteira viajava literalmente.

A correção faz duas coisas. A entrada de sk- agora aceita um corpo delimitado por pontos, então sk-sp-… e sk-ws-… casam por inteiro, e um ponto de fim de frase depois da chave nunca é consumido. Para a Zhipu, foi adicionado um regex deliberadamente com a cara do provider: um id de 32 caracteres em hex minúsculo, um ponto e, em seguida, um segmento de credencial com pelo menos 16 caracteres alfanuméricos. O motivo de não recorrer a uma regra genérica de “token pontuado de alta entropia” são os falsos positivos: nomes de arquivo que são hashes de conteúdo, como <sha>.bundle ou <md5>.sqlite3, não podem casar. Os testes trazem controles negativos exatamente para isso — um sha cru de 40 caracteres hex, um fragmento curto de sk- e um id truncado de 31 caracteres precisam sair idênticos byte a byte.

Medido contra uma sondagem real:

Entrada Antes Depois
api_key: sk-sp-...7890.abcdefgh1234567890_XYZ-0987654321 a cauda vazava em texto claro sk-sp-...4321
api_key: 50aaedFAKE1234567890abcdef1234567890abc.ZpSh99 literal 50aaed...Sh99
o mesmo arquivo passando por file_read a cauda de «redacted:sk-…».abcdefg… sobrevivia «redacted:sk-…» / «redacted-secret»

Essa terceira linha merece um parágrafo próprio: o caminho de “conteúdo de arquivo” não mascara, ele substitui por um sentinela. Há um incidente real por trás disso (issue #35519). Um agente leu uma chave que havia sido mascarada numa forma que parecia uma credencial legítima, porém truncada, gravou-a de volta no config.yaml e destruiu permanentemente a credencial armazenada, transformando-a num valor morto de 13 caracteres — a partir dali, só 401s. O sentinela («redacted-secret») é sintaticamente impossível como token utilizável, então nunca pode ser confundido com um token nem gravado de volta. O desempenho também se manteve: entradas patológicas — um corpo de 20.000 caracteres, 10.000 pontos, 2.000 sequências hex — são redigidas em menos de 3 milissegundos cada.

Parada dois: agora toda faixa de entrega do cron redige, à força

A saída de ferramentas era um caminho. O mais fácil de esquecer é que a saída de um job de cron sai da máquina por design. Na implementação anterior, o stdout e o stderr de um job de script eram limpos onde foram capturados, mas o texto de resposta de um job de LLM chegava ao _deliver_result sem passar por varredura — e todas as faixas abaixo dele enviavam a string bruta.

O PR #114789 coloca a redação no único ponto de estrangulamento da entrega e adiciona um backstop fail-closed:

  • cron/scheduler_delivery.py ganha _redact_cron_payload(), que chama redact_sensitive_text(text, force=True).
  • O que force=True significa: esta é uma fronteira de saída. O security.redact_secrets governa os logs na sua própria máquina; isto aqui governa o que sai dela. Ou seja, a configuração do usuário não consegue desligar isso neste ponto.
  • Falha fechada: se o próprio redator levantar uma exceção, o payload inteiro é substituído por [REDACTED - redaction failed] em vez de passar adiante. Isso segue a postura já usada onde o stdout do shell é capturado.
  • Cobre quatro faixas: a mensagem da plataforma (o ponto de estrangulamento cleaned_delivery_content), o texto do espelho de sessão (que é derivado do content bruto e por isso não herdava o ponto de estrangulamento), o nome do job, que chega a prefixos de espelho e títulos de thread, e o turno de bot chat entregue a outro profile — onde o content é religado para que o registro durável adiado e sua reprodução também carreguem a cópia limpa.
  • A faixa de aviso de falha não precisou de mudança separada: ela sai pelo mesmo ponto de estrangulamento _deliver_result e está coberta por construção.

Uma exceção deliberada vale conhecer: o redact_url_credentials não é ativado para o cron. Esses jobs entregam legitimamente magic links e URLs pré-assinadas no seu próprio chat, e retirar URLs que carregam credenciais de forma indiscriminada quebraria o recurso. É o mesmo julgamento que o agent/redact.py já documenta para as fronteiras de saída de chat.

O que isso ainda não cobre

As duas correções são reais, mas tratam da própria saída do Hermes. Três coisas continuam sendo sua responsabilidade:

  1. Não cole credenciais na conversa. O texto que você mesmo cola também cai no histórico da sessão e no banco. O redator faz o possível com formas conhecidas, mas “formas conhecidas” é sempre uma lista incompleta, e uma chave de terceiros pode não ter nenhuma forma reconhecível. Para conferir um valor de config, use hermes config get <key> e confirme a presença, o comprimento ou os quatro últimos caracteres — não o valor em si.
  2. Não faça echo de arquivos sensíveis em scripts de cron. A faixa de entrega redige, mas os logs do próprio script, a saída da sua CI e o histórico do seu shell ainda podem guardar o original. Imprima o veredito (“KEY presente” / “KEY ausente”), nunca o valor.
  3. Não desligue a redação para expressar frustração. security.redact_secrets: false afeta apenas o caminho de log local e registra um aviso na inicialização; ele não alcança a faixa do cron, que é forçada. Se você estiver depurando o próprio redator, ligue de volta depois.

Para o panorama maior sobre armazenar e rotacionar credenciais, veja o guia de criptografia com keychain. Se a sua faixa de notificação é uma plataforma de chat, o guia de aprovações sem supervisão cobre a outra metade desse controle e, para os jobs em si, comece pelo guia de troubleshooting de cron e pela referência do hermes secrets.

Status de release

Os dois PRs entraram na main em 18 de setembro de 2026, e a tag mais recente é a v0.21.3 (nome de tag v2026.9.14, lançada em 14 de setembro), então nenhum deles está em uma release marcada ainda. Uma instalação via git os pega na próxima atualização:

hermes --version
hermes update

Quem usa imagem Docker ou cloud recebe com a tag seguinte.

O padrão que vale manter

Vistas em conjunto, a regra é simples: a redação é uma lista que precisa se manter atualizada, ela pertence ao ponto único de saída e precisa de um backstop fail-closed. Adicionar um nome à tabela de prefixos não basta se o corpo puder conter pontos; limpar algumas faixas não basta quando o espelho de sessão e o nome do job carregam o mesmo texto para fora; e um redator que, em caso de erro, volta a enviar o texto original não protege nada. Foram essas três costuras que foram corrigidas.