Faça o agente ficar quieto: o pre_llm_call agora pode vetar um turno antes de qualquer chamada ao modelo

Duas da manhã e alguém cola o print de um ticket de suporte numa sessão de bot — telefone do cliente, os quatro últimos dígitos de um documento, tudo. Seu gateway está de olho em uma dúzia de canais. Um segundo depois, aquela mensagem vai para o modelo, entra no transcript e, em seguida, passa pelos resumos de compactação, pelo sistema de memória e pela passada de revisão em background. Quando você pensa “isso nunca deveria ter saído daqui”, já saiu.
Esse é o defeito de qualquer guarda posterior ao fato: você só apaga o dado depois que o modelo respondeu e os logs já foram gravados. Na v0.22.6, o Hermes deu ao hook pre_llm_call um novo valor de retorno que move o ponto de interceptação para antes da requisição de inferência — o turno termina ali, com zero chamadas ao modelo.
O que exatamente é interrompido
pre_llm_call dispara uma vez por turno, depois da compressão de contexto e antes de o loop de tools começar. O trabalho original dele era colocar coisas dentro de um turno: memórias recuperadas, o git status do momento, que dia é hoje. Você retorna uma string ou um objeto com a chave context, e o texto é anexado à mensagem do usuário (nunca ao system prompt, para o cache de prompt sobreviver).
Agora ele também pode recusar o turno:
{"action": "block", "message": "This message contains customer identity data and was blocked by policy. Please handle it in a 1:1 channel."}
Segundo a documentação oficial, é isto o que acontece em seguida:
- Nenhuma requisição de inferência é feita. O turno custa zero chamadas ao modelo.
messageé mostrado ao usuário como resposta do assistente e salvo como resposta do assistente, então o transcript mantém um par normal usuário-depois-assistente e o--resumecontinua funcionando.- O resultado carrega
turn_exit_reason: "blocked_by_plugin_pre_llm_call", para você distinguir “bloqueado” de “o modelo respondeu” nos logs e nos resultados. - O primeiro bloqueio vence; callbacks posteriores não mudam o veredito. Um bloqueio sem mensagem é ignorado.
O que acontece com o texto que você digitou
É esta a parte que decide se o recurso é realmente utilizável como guarda de privacidade.
O prompt bloqueado é gravado no transcript como placeholder:
[Message withheld: a pre_llm_call policy hook blocked it.]
Tudo que lê o histórico depois — turnos seguintes, resumos de compactação, session_search, provedores de memória externos, a revisão de memória em background — lê esse placeholder em vez do original. Em outras palavras, o conteúdo nunca chega a um modelo neste turno nem em nenhum outro depois.
O que você de fato digitou sobrevive apenas como metadado de exibição: o recap do --resume na CLI, a TUI, o app desktop e o dashboard ainda mostram, então você consegue confirmar depois o que foi pedido.
Dois efeitos colaterais que a documentação também deixa explícitos: o hook roda antes da compactação de início de turno, então um turno bloqueado pula a compactação, a titulagem da sessão e o prefetch/sync de memória externa. Para um turno que não aconteceu, nada desse trabalho valeria a pena mesmo.
Escrevendo um: plugin Python ou hook de shell
Plugin Python (registre e já está no ar):
import re
CUSTOMER_ID = re.compile(r"\b(?:cust|acct)-[0-9]{6,}\b", re.I)
def policy(session_id, user_message, platform, **kwargs):
text = user_message if isinstance(user_message, str) else ""
if CUSTOMER_ID.search(text):
return {
"action": "block",
"message": "This message contains a customer identifier and was blocked by policy. Handle it in the ticket system, or resend a redacted description.",
}
return None
def register(ctx):
ctx.register_hook("pre_llm_call", policy)
Hook de shell (no config.yaml do profile, sem Python):
hooks:
pre_llm_call:
- command: "~/.hermes/agent-hooks/block-customer-ids.sh"
timeout: 5
O script recebe o payload do evento no stdin e imprime uma linha de JSON no stdout. Tanto o formato {"action": "block", "message": "..."} quanto o estilo Claude Code {"decision": "block", "reason": "..."} são aceitos; imprima {} quando nada deve ser bloqueado.
Três fronteiras que você precisa conhecer
Um: ele falha aberto. Se um callback levanta exceção, ou passa de plugins.hook_callback_timeout (30 segundos por padrão, configurável de 0 a 600), o Hermes registra no log e o pula — o turno segue. Compare com pre_tool_call, onde um timeout falha fechado e bloqueia a tool. A documentação é explícita sobre a consequência: um hook de política deve capturar os próprios erros e retornar um bloqueio quando não consegue decidir, senão um script quebrado se parece com uma guarda enquanto não bloqueia nada.
Relacionado: fail_closed: true hoje vale só para pre_tool_call. Definir isso em outro evento gera um aviso no parse da config e é ignorado.
Dois: código de saída 2 não é bloqueio aqui. Essa convenção pertence só ao pre_tool_call. Um hook de shell pre_llm_call que sai com 2 é tratado como qualquer outra saída não-zero: avisa e ainda tenta ler o stdout.
Três: isto é um filtro de entrada, não um filtro de conteúdo. Ele vê o user_message deste turno e o histórico da conversa — não quais tools o modelo está prestes a chamar. Bloquear tools é trabalho do pre_tool_call; julgar o que o modelo disse é do post_llm_call. Você quer as duas camadas: a entrada barra o que nunca deveria chegar, a camada de tools barra ações perigosas.
Quando vale a pena ligar
Três casos se pagam na hora:
- Privacidade e compliance logo na porta. IDs de ticket, números de documento, hostnames internos, e-mails de clientes: casar e bloquear, com uma mensagem que diz à pessoa qual canal usar em vez desse.
- Um gate de custo. Quando a cota está quase no fim, ou é madrugada, recuse as requisições que arrastariam uma recuperação enorme, em vez de deixar o modelo responder metade e abortar.
- Defesa contra prompt injection. Páginas web ou e-mails colados carregando padrões óbvios de injeção (“ignore all previous instructions”) voltam antes de o modelo sequer vê-los.
O recurso está na tag v0.22.6, então atualizar para essa versão já basta. A lista completa de eventos, os contratos de retorno e as regras de timeout estão na documentação oficial de hooks. Se você quer primeiro a distinção entre hooks e middleware, nosso guia de hooks e ciclo de vida de plugins é um bom ponto de partida, e para pontos de pausa que precisam de um humano, veja o post sobre hooks de entrada humana.