Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Hooks de eventos

O Hermes tem quatro sistemas de hook que rodam código próprio em pontos-chave do ciclo de vida:

SistemaRegistrado porRoda emPara que serve
Hooks do gatewayHOOK.yaml + handler.py em ~/.hermes/hooks/Só no gatewayRegistro em log, alertas, webhooks
Hooks de pluginctx.register_hook() num pluginCLI + gatewayInterceptação de ferramentas, métricas, salvaguardas
Hooks de shellbloco hooks: em ~/.hermes/config.yaml apontando para scripts de shellCLI + gatewayScripts prontos para bloquear, formatar automaticamente, injetar contexto
Webhooks de saídalista hooks.outbound: em ~/.hermes/config.yamlCLI + gatewayEmpurrar eventos de ciclo de vida assinados para endpoints HTTP externos — CI, painéis, outros agentes

Erros em callbacks de hook são isolados e registrados em log, em vez de derrubar o agente. Nem todos os hooks são passivos: hooks de diretiva/controle podem mudar o fluxo, transformações podem substituir conteúdo, e um hook de shell em pre_tool_call pode bloquear ou falhar fechado.

Hooks de eventos do gateway​

Os hooks do gateway disparam automaticamente durante a operação do gateway (Telegram, Discord, Slack, WhatsApp, Teams), sem travar o pipeline principal do agente.

Criar um hook​

Cada hook é um diretório em ~/.hermes/hooks/ com dois arquivos:

~/.hermes/hooks/
└── meu-hook/
├── HOOK.yaml # Declara em quais eventos escutar
└── handler.py # Função Python que trata o evento

HOOK.yaml​

name: meu-hook
description: Registra em arquivo toda a atividade do agente
events:
- agent:start
- agent:end
- agent:step

A lista events determina quais eventos acionam o seu handler. Você pode assinar qualquer combinação de eventos, inclusive curingas como command:*.

handler.py​

import json
from datetime import datetime
from pathlib import Path

LOG_FILE = Path.home() / ".hermes" / "hooks" / "meu-hook" / "activity.log"

async def handle(event_type: str, context: dict):
"""Chamada a cada evento assinado. Precisa se chamar 'handle'."""
entry = {
"timestamp": datetime.now().isoformat(),
"event": event_type,
**context,
}
with open(LOG_FILE, "a") as f:
f.write(json.dumps(entry) + "\n")

Regras do handler:

  • Precisa se chamar handle
  • Recebe event_type (string) e context (dicionário)
  • Pode ser async def ou def normal — os dois funcionam
  • Erros são capturados e registrados, e nunca derrubam o agente

Eventos disponíveis​

EventoQuando disparaChaves do contexto
gateway:startupO processo do gateway iniciaplatforms (lista com os nomes das plataformas ativas)
session:startNova sessão de mensagens criadaplatform, user_id, session_id, session_key
session:endSessão encerrada (antes do reset)platform, user_id, session_key
session:resetO usuário rodou /new ou /resetplatform, user_id, session_key
session:compressA compressão de contexto de uma sessão terminouplatform, session_id, old_session_id (vazio quando a compactação foi feita no lugar), in_place (booleano — true = transcrição compactada no mesmo id, false = rotacionada a partir de old_session_id), compression_count
agent:startO agente começa a processar uma mensagemplatform, user_id, chat_id, thread_id (id do tópico de fórum / da raiz da thread; vazio fora de uma thread), chat_type ("dm" | "group" | "forum"; vazio se desconhecido), session_id, message (truncada em 500 caracteres)
agent:stepCada iteração do laço de chamada de ferramentasplatform, user_id, session_id, iteration, tool_names
agent:endO agente termina o processamentoas mesmas chaves de agent:start, mais response (truncada em 500 caracteres)
reaction:addedUma reação de emoji foi adicionada a uma mensagem que o bot enxerga (hoje, no adaptador do Slack). Exige o escopo reactions:read e a assinatura do evento de bot reaction_added; o bot precisa ser membro do canal.platform, reaction, user_id, item_user_id, item_type, channel_id, message_ts, team_id, event_ts, raw_event
reaction:removedUma reação de emoji foi removida de uma mensagem que o bot enxerga. Exige a assinatura do evento de bot reaction_removed.mesmo formato de reaction:added
command:*Qualquer comando de barra executadoplatform, user_id, command, args

Correspondência por curinga​

Handlers registrados em command:* disparam para qualquer evento command: (command:model, command:reset etc.). Monitore todos os comandos de barra com uma única assinatura.

Respostas em thread

Um handler que publica uma mensagem de acompanhamento no mesmo tópico de fórum do Telegram deve incluir message_thread_id=int(thread_id) quando chat_type == "forum" e thread_id não estiver vazio.

Exemplos​

Alerta no Telegram em tarefas longas​

Mande uma mensagem para você mesmo quando o agente passar de 10 passos:

# ~/.hermes/hooks/long-task-alert/HOOK.yaml
name: long-task-alert
description: Avisa quando o agente está dando muitos passos
events:
- agent:step
# ~/.hermes/hooks/long-task-alert/handler.py
import os
import httpx

THRESHOLD = 10
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_HOME_CHANNEL")

async def handle(event_type: str, context: dict):
iteration = context.get("iteration", 0)
if iteration == THRESHOLD and BOT_TOKEN and CHAT_ID:
tools = ", ".join(context.get("tool_names", []))
text = f"⚠️ O agente já está rodando há {iteration} passos. Últimas ferramentas: {tools}"
async with httpx.AsyncClient() as client:
await client.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={"chat_id": CHAT_ID, "text": text},
)

Registro de uso de comandos​

Acompanhe quais comandos de barra são usados:

# ~/.hermes/hooks/command-logger/HOOK.yaml
name: command-logger
description: Registra o uso de comandos de barra
events:
- command:*
# ~/.hermes/hooks/command-logger/handler.py
import json
from datetime import datetime
from pathlib import Path

LOG = Path.home() / ".hermes" / "logs" / "command_usage.jsonl"

def handle(event_type: str, context: dict):
LOG.parent.mkdir(parents=True, exist_ok=True)
entry = {
"ts": datetime.now().isoformat(),
"command": context.get("command"),
"args": context.get("args"),
"platform": context.get("platform"),
"user": context.get("user_id"),
}
with open(LOG, "a") as f:
f.write(json.dumps(entry) + "\n")

Webhook no início de sessão​

Faça um POST para um serviço externo quando surgir uma sessão nova:

# ~/.hermes/hooks/session-webhook/HOOK.yaml
name: session-webhook
description: Avisa um serviço externo quando há sessão nova
events:
- session:start
- session:reset
# ~/.hermes/hooks/session-webhook/handler.py
import httpx

WEBHOOK_URL = "https://seu-servico.example.com/hermes-events"

async def handle(event_type: str, context: dict):
async with httpx.AsyncClient() as client:
await client.post(WEBHOOK_URL, json={
"event": event_type,
**context,
}, timeout=5)

Tutorial: BOOT.md — rode uma checklist de partida a cada boot do gateway​

Um padrão popular na comunidade: deixe uma checklist em Markdown em ~/.hermes/BOOT.md e faça o agente executá-la uma vez a cada vez que o gateway inicia. Serve para coisas como "a cada boot, verifique falhas de cron durante a noite e me avise no Discord se algo falhou" ou "resuma as últimas 24h do deploy.log e publique no canal #ops do Slack".

Este tutorial mostra como montar isso você mesmo, como um hook definido por você. O Hermes não traz um hook BOOT.md embutido — você liga exatamente o comportamento que quer.

O que vamos construir​

  1. Um arquivo em ~/.hermes/BOOT.md com instruções de partida em linguagem natural.
  2. Um hook de gateway que dispara em gateway:startup, cria um agente de uso único com o modelo e as credenciais já resolvidos do seu gateway, e executa as instruções do BOOT.md.
  3. Uma convenção [SILENT] para o agente poder optar por não mandar mensagem quando não há nada a relatar.

Passo 1: escreva a sua checklist​

Crie o ~/.hermes/BOOT.md. Escreva como se estivesse instruindo um assistente humano:

# Checklist de partida

1. Rode `hermes cron list` e verifique se alguma tarefa agendada falhou durante a noite.
2. Se alguma falhou, resuma para o canal #ops do Discord (o hook entrega a sua resposta final ao destino configurado).
3. Verifique se `/opt/app/deploy.log` tem linhas com ERROR nas últimas 24 horas. Se tiver, resuma e inclua no mesmo relatório.
4. Se nada deu errado, responda apenas `[SILENT]` para que nenhuma mensagem seja enviada.

O agente vê isso como parte do prompt dele, então vale tudo que você consegue descrever em linguagem simples — chamadas de ferramenta, comandos de shell, envio de mensagens, resumo de arquivos.

Passo 2: crie o hook​

~/.hermes/hooks/boot-md/
├── HOOK.yaml
└── handler.py

~/.hermes/hooks/boot-md/HOOK.yaml

name: boot-md
description: Roda o ~/.hermes/BOOT.md na partida do gateway
events:
- gateway:startup

~/.hermes/hooks/boot-md/handler.py

"""Roda o ~/.hermes/BOOT.md a cada partida do gateway."""

import logging
import threading
from pathlib import Path

logger = logging.getLogger("hooks.boot-md")

BOOT_FILE = Path.home() / ".hermes" / "BOOT.md"


def _build_prompt(content: str) -> str:
return (
"You are running a startup boot checklist. Follow the instructions "
"below exactly.\n\n"
"---\n"
f"{content}\n"
"---\n\n"
"Execute each instruction. Put any user-facing summary in your "
"final response — the hook delivers it to the configured channel "
"(e.g. Discord or Slack); you do not send messages yourself.\n"
"If nothing needs attention and there is nothing to report, reply "
"with ONLY: [SILENT]"
)


def _run_boot_agent(content: str) -> None:
"""Cria um agente de uso único e executa a checklist.

Usa o modelo já resolvido do gateway e as credenciais de execução, para que
isso funcione igual com endpoints personalizados, agregadores e provedores
baseados em OAuth.
"""
try:
from gateway.run import _resolve_gateway_model, _resolve_runtime_agent_kwargs
from run_agent import AIAgent

agent = AIAgent(
model=_resolve_gateway_model(),
**_resolve_runtime_agent_kwargs(),
platform="gateway",
quiet_mode=True,
skip_context_files=True,
skip_memory=True,
max_iterations=20,
)
result = agent.run_conversation(_build_prompt(content))
response = (result.get("final_response", "") or "").strip()
if response.upper() not in {"[SILENT]", "SILENT", "NO_REPLY", "NO REPLY"}:
logger.info("boot-md completed: %s", response[:200])
else:
logger.info("boot-md completed (nothing to report)")
except Exception as e:
logger.error("boot-md agent failed: %s", e)


async def handle(event_type: str, context: dict) -> None:
if not BOOT_FILE.exists():
return
content = BOOT_FILE.read_text(encoding="utf-8").strip()
if not content:
return

logger.info("Running BOOT.md (%d chars)", len(content))

# Thread em segundo plano, para a partida do gateway não ficar presa a um turno inteiro do agente.
thread = threading.Thread(
target=_run_boot_agent,
args=(content,),
name="boot-md",
daemon=True,
)
thread.start()

As duas linhas mais importantes:

  • _resolve_gateway_model() lê o modelo configurado no gateway naquele momento.
  • _resolve_runtime_agent_kwargs() resolve as credenciais do provedor do mesmo jeito que um turno normal do gateway — incluindo chaves de API, URLs-base, tokens OAuth e pools de credenciais.

Sem isso, um AIAgent() pelado cai nos padrões embutidos e vai tomar 401 em qualquer endpoint que não seja o padrão.

Passo 3: teste​

Reinicie o gateway:

hermes gateway restart

Acompanhe os logs:

hermes logs --follow --level INFO | grep boot-md

Você deve ver Running BOOT.md (N chars) seguido de boot-md completed: ... (resumo do que o agente fez) ou de boot-md completed (nothing to report), quando o agente respondeu com um token de silêncio exato como [SILENT].

Apague o ~/.hermes/BOOT.md para desativar a checklist — o hook continua carregado, mas passa batido em silêncio quando o arquivo não está lá.

Estender o padrão​

  • Checklists que sabem o dia: use datetime.now().weekday() dentro das instruções do BOOT.md ("se for segunda-feira, verifique também o log semanal de deploy"). As instruções são texto livre, então vale qualquer coisa sobre a qual o agente consiga raciocinar.
  • Várias checklists: aponte o hook para outro arquivo (STARTUP.md, MORNING.md etc.) e registre um diretório de hook separado para cada um.
  • Variante sem agente: se você não precisa de um laço completo de agente, dispense o AIAgent e faça o handler publicar direto uma notificação fixa via httpx. Mais barato, mais rápido, e sem depender de provedor.

Por que isso não é embutido​

Uma versão antiga do Hermes trazia isso como hook embutido e criava, em silêncio, um agente com os padrões pelados a cada boot do gateway. Isso pegou de surpresa quem usava endpoints personalizados e deixou o recurso invisível para quem nem sabia que ele estava rodando. Manter isso como um padrão documentado — montado por você, no seu diretório de hooks — significa que você vê exatamente o que ele faz e adere escrevendo os arquivos.

Como funciona​

  1. Na partida do gateway, o HookRegistry.discover_and_load() varre o ~/.hermes/hooks/
  2. Cada subdiretório com HOOK.yaml + handler.py é carregado dinamicamente
  3. Os handlers são registrados nos eventos que declararam
  4. Em cada ponto do ciclo de vida, o hooks.emit() dispara todos os handlers correspondentes
  5. Erros em qualquer handler são capturados e registrados — um hook quebrado nunca derruba o agente
informação

Hooks de gateway só disparam no gateway (Telegram, Discord, Slack, WhatsApp, Teams). A CLI não carrega hooks de gateway. Para hooks que funcionam em todo lugar, use hooks de plugin.

Hooks de plugin​

Plugins podem registrar hooks que disparam em sessões tanto da CLI quanto do gateway. Eles são registrados por código, com ctx.register_hook(), dentro da função register() do seu plugin.

Para detalhes de empacotamento e registro de plugins, veja o guia de plugins.

def register(ctx):
ctx.register_hook("pre_tool_call", my_tool_observer)
ctx.register_hook("post_tool_call", my_tool_logger)
ctx.register_hook("pre_llm_call", my_memory_callback)
ctx.register_hook("post_llm_call", my_sync_callback)
ctx.register_hook("on_session_start", my_init_callback)
ctx.register_hook("on_session_end", my_cleanup_callback)
# Ciclo de vida do quadro kanban (a espera por dependência pode disparar dentro da transação dela):
ctx.register_hook("kanban_task_claimed", my_claim_callback) # processo despachante
ctx.register_hook("kanban_task_completed", my_done_callback) # processo trabalhador
ctx.register_hook("kanban_task_blocked", my_blocked_callback) # processo trabalhador

Regras gerais para todos os hooks:

  • Os callbacks recebem argumentos nomeados. Aceite sempre **kwargs, por compatibilidade futura.
  • Exceções em callback são registradas e o callback é pulado; os callbacks seguintes continuam.
  • Se um callback Python de plugin num hook com limite de tempo (observadores de caminho quente, como post_tool_call / pre_llm_call, mais o hook de política pre_tool_call) travar por mais que plugins.hook_callback_timeout (padrão 30s, 0 desativa, máximo 600), ele é abandonado sem esperar o worker, para o laço do agente continuar. Callbacks de pre_tool_call que estouraram o tempo ou ainda estão rodando falham fechado (bloqueiam a ferramenta); os outros hooks com limite falham abertos (são pulados). Hooks com contrato documentado de thread chamadora (subagent_stop) nunca são movidos para um worker com limite de tempo. Hooks de shell mantêm o timeout próprio de cada entrada.
  • O catálogo abaixo é descritivo: observadores ignoram o retorno, transformações aceitam a primeira substituição válida em string, e hooks de diretiva/controle consomem formatos de retorno documentados. Middleware de plugin é um registro e uma superfície à parte, não mais uma categoria de hook.
  • Campos de correlação como turn_id, api_request_id, task_id, session_id e api_call_count são específicos de cada hook e podem não existir. Trate os IDs como opacos.
  • A validade dos nomes de evento em tempo de execução vem de hermes_cli.plugins.VALID_HOOKS. O hermes hooks list lista os hooks de shell e de saída configurados, não todo evento disponível; o hermes hooks test <event> só informa o conjunto válido quando você passa um evento inválido.

Seções de prompt do sistema seguras para cache​

Plugins que precisam de orientação durável e sempre ativa podem registrar uma seção limitada do prompt do sistema, em vez de injetar o mesmo texto pelo pre_llm_call a cada turno:

def board_rules(session_info):
return f"Apply the worker rules for profile {session_info['profile_name']}."

def register(ctx):
ctx.register_system_prompt_section(
"kanban-advanced.worker-rules",
board_rules, # uma string também é aceita
position="after_memory",
max_chars=4000,
)

O contrato é propositalmente estreito:

  • Os IDs são globais, estáveis, identificadores minúsculos de 1 a 128 caracteres, usando só letras, números, ., _ e -. IDs duplicados são rejeitados.
  • after_memory é a única âncora de posicionamento. As seções são ordenadas por ID, renderizadas depois do contexto de memória/perfil e antes dos metadados de sessão; plugins não conseguem reordenar nem substituir o conteúdo central do prompt.
  • Um callable recebe um mapa somente leitura com session_id, model, provider, platform, profile_name e cwd. Ele roda uma vez por sessão nova. Os bytes renderizados dele são congelados na compressão e recuperados do prompt do sistema completo já persistido depois de um reinício/retomada do processo; o estado do plugin não é lido de novo numa sessão existente.
  • O max_chars é limitado a 4.000 caracteres. Todas as seções de plugin juntas, incluindo os cabeçalhos de auditoria delas, são limitadas a 8.000 caracteres e 32 seções. Seções vazias, que não sejam string, grandes demais, que estourem o orçamento agregado ou que levantem exceção são puladas com um aviso; a construção do prompt continua.
  • Toda seção aceita é nomeada no prompt e registrada em log no início da sessão, com o plugin, a posição e a contagem de caracteres.

Use o pre_llm_call para contexto realmente dinâmico, turno a turno. De propósito, não existe neste contrato um hook de plugin para dicas de ambiente: mudar o cwd, o branch ou outros dados de ambiente não pode mutar em silêncio o prompt em cache de uma sessão. Um hook desses precisa de um consumidor concreto e das mesmas semânticas de congelamento e retomada segura antes de poder existir.

Catálogo dos hooks de plugin entregues​

Os campos de payload abaixo são exatamente os campos específicos de evento fornecidos por cada ponto de chamada. Por compatibilidade retroativa, o PluginManager também acrescenta telemetry_schema_version="hermes.observer.v1" a todo callback de hook de plugin. Esse marcador de envelope legado não significa que todos os payloads de hook compartilham um esquema semântico; contratos novos versionados pertencem ao evento concreto ou à família de capacidade deles.

HookCategoriaMomento exato e comportamento de retornoCampos explícitos do payloadPrivacidade / sensibilidade
pre_tool_callDiretiva/controleUma vez antes da execução; vence a primeira diretiva block ou approve válida, e retornos modify são mesclados de forma rasa nos argumentos da ferramenta.tool_name, args, task_id, session_id, tool_call_id, turn_id, api_request_id, middleware_traceOs argumentos crus podem conter conteúdo do usuário, caminhos, comandos ou segredos.
post_tool_callObservadorDepois de resultado bloqueado, com erro ou bem-sucedido; o retorno é ignorado.tool_name, args, result, task_id, session_id, tool_call_id, turn_id, api_request_id, duration_ms, status, error_type, error_message, middleware_traceO texto de resultado/erro pode conter conteúdo arbitrário de ferramenta ou do usuário, e segredos.
transform_tool_resultTransformaçãoDepois do post_tool_call, antes de anexar à conversa; a primeira string substitui o resultado.tool_name, args, result, task_id, session_id, tool_call_id, turn_id, api_request_id, duration_ms, status, error_type, error_messageExpõe o resultado completo destinado ao modelo e os argumentos.
transform_terminal_outputTransformaçãoDepois da captura limitada do processo em primeiro plano, antes da limitação final da saída; a primeira string substitui a saída.command, output, returncode, task_id, env_typeComando e saída podem conter credenciais.
pre_transcriptionTransformaçãoDisparado pelo despachante de STT depois de resolver o provedor e antes de invocar qualquer backend (embutido, do tipo comando ou registrado por plugin); resultados em dicionário são aplicados na ordem de registro, valendo quem escreve por último em cada campo (prompt, language, model; file_path é somente leitura).file_path, provider, model, language, prompt, sourceO prompt final é enviado ao provedor de STT configurado junto com o áudio — não devolva segredos no hook.
pre_llm_callDiretiva/controleUma vez por turno, antes do laço; todos os retornos válidos em string ou {"context": ...} são juntados e injetados na mensagem do usuário.session_id, task_id, turn_id, user_message, conversation_history, is_first_turn, model, platform, parent_session_id, sender_idMensagem do usuário e histórico da conversa completos.
post_llm_callObservadorFinalização de um turno bem-sucedido e não interrompido; o retorno é ignorado.session_id, task_id, turn_id, user_message, assistant_response, conversation_history, model, platformPrompt, resposta e histórico completos.
transform_llm_outputTransformaçãoAntes do post_llm_call e da entrega final; a primeira string não vazia substitui a resposta.response_text, session_id, model, platformTexto final completo do assistente.
pre_verifyDiretiva/controleNo portão limitado de verificação de código editado; a primeira diretiva válida de continuar/bloquear-a-parada mantém o turno andando.session_id, platform, model, coding, attempt, final_response, changed_pathsRascunho da resposta e caminhos alterados.
pre_api_requestObservadorA cada tentativa de provedor, imediatamente antes da requisição; o retorno é ignorado.task_id, turn_id, api_request_id, session_id, user_message, conversation_history, platform, model, provider, base_url, api_mode, api_call_count, retry_count, request_messages, message_count, tool_count, approx_input_tokens, request_char_count, max_tokens, started_at, middleware_trace, requestSensibilidade alta: os campos legados user_message, conversation_history e request_messages são crus de propósito; prefira o request já saneado.
post_api_requestObservadorDepois do sucesso normalizado do provedor; o retorno é ignorado.task_id, turn_id, api_request_id, session_id, platform, model, provider, base_url, api_mode, api_call_count, api_duration, started_at, ended_at, finish_reason, message_count, response_model, response, usage, assistant_message, assistant_content_chars, assistant_tool_call_countO response saneado está disponível, mas o assistant_message normalizado cru pode conter conteúdo do modelo ou do usuário; usage é dado contábil.
api_request_errorObservadorA cada tentativa de provedor que falha; o retorno é ignorado.task_id, turn_id, api_request_id, session_id, platform, model, provider, base_url, api_mode, api_call_count, api_duration, started_at, ended_at, status_code, retry_count, max_retries, retryable, reason, error, requestO texto de erro pode conter dados do provedor ou do usuário; o request é feito para vir saneado.
on_stream_startObservadorDespachado quando começa uma resposta de LLM em streaming; entregue fora do caminho dos tokens, por uma fila limitada do host com um worker por callback; o retorno é ignorado.turn_id, iteration, session_id, model, provider, surfaceSó identificadores e metadados de roteamento.
on_stream_deltaObservadorDespachado a cada delta de texto normalizado do streaming, pela fila limitada de observadores; um callback travado descarta apenas os eventos mais antigos dele; o retorno é ignorado.delta, kind (text ou reasoning), turn_id, iteration, session_id, model, provider, surfaceO texto do delta é saída crua do modelo; deltas de raciocínio exigem a adesão explícita em plugins.stream_reasoning_deltas.
on_stream_endObservadorDespachado quando uma resposta em streaming termina ou dá erro, depois que o fluxo fecha; o retorno é ignorado.final_text, finished, error, turn_id, iteration, session_id, model, provider, surfaceTexto completo da resposta montada; o texto de erro pode incluir dados do provedor.
on_interim_messageObservadorDespachado quando uma mensagem do assistente no meio do laço aparece antes da resposta final (com ou sem streaming); o retorno é ignorado.text, already_streamed, turn_id, iteration, session_id, model, provider, surfaceTexto intermediário completo do assistente.
transform_api_error_classificationTransformaçãoA cada tentativa de provedor que falha, no topo do classificador embutido; todos os callbacks rodam, e vence o primeiro dicionário com um reason válido (roda-todos-depois-escolhe-o-primeiro); resultados válidos descartados geram um aviso em tempo de execução. Só plugins Python.provider, model, status_code, error_type, error_code, error_message, error_body, error, approx_tokens, context_length, num_messageserror_message e error_body podem conter dados crus do provedor ou do usuário.
on_session_startObservadorPrimeiro turno de uma sessão nova; o retorno é ignorado.session_id, model, platformSó identificadores e metadados de roteamento.
on_session_endObservadorCanonicamente, a cada finalização de turno; saídas da CLI/TUI têm formatos legados reduzidos adicionais. O retorno é ignorado.Canônico: session_id, task_id, turn_id, completed, failed, interrupted, turn_exit_reason, model, platform; caminhos de saída podem acrescentar reason/api_request_id e omitir campos.IDs, modelo/plataforma e desfecho; o payload canônico não traz corpo de mensagem.
on_session_finalizeObservadorDesmontagem de CLI/TUI/gateway pelo finalize_session; o desligamento ou a expiração do gateway podem finalizar sem um reset. O retorno é ignorado.Depende da superfície: session_id, platform, e opcionalmente reason, old_session_id, new_session_idIdentificadores de sessão e de roteamento.
on_session_resetObservadorFronteira de sessão na CLI/TUI e, no gateway, depois que a sessão substituta existe; o retorno é ignorado.CLI: session_id, platform, reason; TUI: session_id, platform; gateway: esses mais reason, old_session_id, new_session_idIdentificadores de sessão e de roteamento.
on_skill_lifecycleObservadorDepois de uma mudança autoritativa de estado de uso de skill; o retorno é ignorado.action, skill_name, provenance, task_id, session_id, use_count, reused, reuse_after_patchExpõe o nome local da skill e a procedência.
subagent_startObservadorFilho construído e prestes a rodar; o retorno é ignorado.parent_session_id, parent_turn_id, parent_subagent_id, child_session_id, child_subagent_id, child_role, child_goalO objetivo do filho pode conter conteúdo do usuário ou do projeto.
subagent_stopObservadorSaída do filho; o retorno é ignorado.parent_session_id, parent_turn_id, child_session_id, child_role, child_summary, child_status, tool_call_history, duration_msO resumo e os metadados de histórico de ferramentas já redigidos podem revelar a estrutura do projeto.
pre_gateway_dispatchDiretiva/controleMensagem recebida não interna, antes de autenticação/pareamento/despacho; o primeiro skip, rewrite ou allow válido controla o fluxo.event, gateway, session_storeObjetos em processo extremamente privilegiados, que expõem dados de entrada do usuário e de roteamento, além de handles do host.
gateway_platform_eventObservadorDepois que a autorização do gateway com escopo de perfil passa, quando um evento nativo de plataforma suportado é normalizado na fronteira do gateway (Telegram: reações, edições de mensagem; Discord: edições/exclusões de mensagem, thread criada/renomeada); o retorno é ignorado.platform, event_type, payload (dicionário específico do tipo de evento — veja os contratos por evento mais abaixo)Só um envelope normalizado de dicionário simples; objetos crus do SDK, handles de adaptador e clientes de bot nunca são expostos.
pre_commandObservadorComando de barra reconhecido prestes a ser despachado, antes de o handler rodar, no despacho de caminho frio da CLI e do gateway; o retorno é ignorado na v1 (dicionários em formato de diretiva são registrados em nível debug). Os comandos de interceptação de agente em execução no gateway (/stop, /approve durante uma execução ativa) foram deliberadamente excluídos — as saídas de emergência do plano de controle precisam ficar fora do alcance de plugins.surface ("cli" | "gateway"), command (nome canônico), alias_used, args_raw, session_key, platformargs_raw pode conter conteúdo do usuário ou segredos digitados depois do comando.
pre_approval_requestObservadorAntes da aprovação pedida ou inteligente; o retorno é ignorado.command, description, pattern_key, pattern_keys, session_key, surface, turn_id, tool_call_idO comando pode conter segredos; a preparação do observador inteligente força a redação, mas nem todas as superfícies têm redação idêntica.
post_approval_responseObservadorDepois de uma decisão, de um tempo esgotado ou de uma falha de notificação do gateway; o retorno é ignorado.command, description, pattern_key, pattern_keys, session_key, surface, turn_id, tool_call_id, choice; o caminho inteligente pode acrescentar decided_byA mesma sensibilidade do comando, mais os metadados da decisão.
kanban_task_claimedObservadorDepois do commit da reivindicação, no processo despachante, antes de criar o trabalhador; o retorno é ignorado.task_id, profile_name, board, assignee, run_idIdentificadores de quadro/tarefa/perfil/responsável.
kanban_task_completedObservadorDepois da conclusão e da limpeza, em geral no processo trabalhador; o retorno é ignorado.task_id, profile_name, board, assignee, run_id, summaryO resumo pode conter conteúdo do projeto ou do usuário.
kanban_task_blockedObservadorDepois de uma transição para bloqueado; o caminho de espera por dependência dispara antes de a transação dele sair. O retorno é ignorado.task_id, profile_name, board, assignee, run_id, reasonO motivo pode conter conteúdo do projeto ou do usuário.
on_kanban_worker_spawnedObservadorDepois que o spawn_fn retorna e o PID do trabalhador é persistido; roda dentro do lock de despacho, mantenha os callbacks rápidos. O retorno é ignorado.task_id, profile_name, board, assignee, run_id, worker_pid, workspace_pathworkspace_path é um caminho de sistema de arquivos e pode revelar a organização do projeto ou nomes de usuário.
on_kanban_worker_exitedObservadorDerivado do tique: depois que o detect_crashed_workers recupera uma tarefa de PID morto e a recuperação é commitada. O retorno é ignorado.task_id, profile_name, board, assignee, run_id, worker_pid, exit_kind, exit_code, outcome, retry_statusSó identificadores e metadados de saída.
on_kanban_worker_stale_claimObservadorDepois que uma reivindicação com TTL vencido é recuperada; extensões com PID vivo não disparam. O retorno é ignorado.task_id, profile_name, board, assignee, run_id, worker_pid, heartbeat_stale, retry_statusSó identificadores e metadados de reivindicação.
on_kanban_task_updatedObservadorDepois de uma escrita commitada em campo de tarefa fora do ciclo de reivindicar/concluir/bloquear (atribuição, sobrescritas, editores do painel). O retorno é ignorado.task_id, profile_name, board, assignee, run_id, changed_fieldschanged_fields traz só nomes de campo, nunca valores; os valores de título/corpo nomeados no banco do quadro podem conter conteúdo do usuário ou do projeto.
on_kanban_dispatch_tickObservadorUma vez por tique do despachante, estritamente depois de o lock de despacho ser liberado; tiques ociosos e disputados também disparam. O retorno é ignorado.board, profile_name, dry_run, outcome, resultresult é o DispatchResult do tique e carrega ids de tarefa, responsáveis e caminhos de espaço de trabalho.

Hooks de saída em streaming​

Estes hooks, só de observação, deixam plugins consumirem a saída em streaming do LLM para telemetria, painéis ao vivo ou pipelines de TTS, sem alterar a resposta. Eles são entregues por filas limitadas do host, com um worker em segundo plano por callback registrado, então callbacks de plugin nunca rodam em linha no caminho dos tokens. Se um callback trava, só a fila daquele callback pode encher e descartar o evento de observador pendente mais antigo dele; os outros observadores continuam recebendo eventos normalmente.

Registre-os como qualquer outro hook de plugin:

def on_delta(delta, kind, model, provider, **kwargs):
if kind == "text":
print(delta, end="", flush=True)

def register(ctx):
ctx.register_hook("on_stream_delta", on_delta)

Campos comuns aos quatro hooks:

ParâmetroTipoDescrição
turn_idstrIdentificador opaco do turno, quando existe
iterationintIteração atual da chamada de API / do laço de ferramentas
session_idstrId da sessão atual do Hermes
modelstrIdentificador do modelo ativo
providerstrNome do provedor ativo
surfacestrSuperfície chamadora, por exemplo cli, discord, telegram

Campos adicionais:

HookCampos extras
on_stream_startnenhum
on_stream_deltadelta: str, `kind: "text"
on_stream_endfinal_text: str, finished: bool, `error: str
on_interim_messagetext: str, already_streamed: bool

O on_interim_message também pode disparar depois de uma resposta sem streaming, então registrar só esse hook não força uma chamada de provedor para o transporte de streaming.

Deltas de raciocínio não são expostos a plugins por padrão. Adira explicitamente:

plugins:
stream_reasoning_deltas: true

Os valores de retorno são ignorados. Para o fluxo continuar rápido, os callbacks devem enfileirar o próprio trabalho e retornar logo. Exceções são registradas e não param o fluxo.


pre_tool_call​

Dispara imediatamente antes de toda execução de ferramenta — tanto ferramentas embutidas quanto ferramentas de plugin.

Assinatura do callback:

def my_callback(tool_name: str, args: dict, task_id: str, **kwargs):
ParâmetroTipoDescrição
tool_namestrNome da ferramenta prestes a executar (por exemplo "terminal", "web_search", "read_file")
argsdictOs argumentos que o modelo passou para a ferramenta
task_idstrIdentificador de sessão/tarefa. String vazia se não estiver definido.

Dispara: em model_tools.py, dentro de handle_function_call(), antes de o handler da ferramenta rodar. Dispara uma vez por chamada de ferramenta — se o modelo chama 3 ferramentas em paralelo, dispara 3 vezes.

Valor de retorno — bloquear ou exigir aprovação:

return {"action": "block", "message": "Motivo pelo qual a chamada foi bloqueada"}
# ou
return {"action": "approve", "message": "Por que a aprovação é necessária", "rule_key": "optional:scope"}

Vence a primeira diretiva válida (primeiro os plugins Python registrados, depois os hooks de shell). O block exige uma message não vazia e interrompe a ferramenta, devolvendo esse texto ao modelo como erro. O approve eleva a chamada ao portão de aprovação humana que já existe; message e rule_key são opcionais, e negação, tempo esgotado ou erro do portão falham fechado. Os demais valores de retorno são ignorados, então callbacks que só observam continuam funcionando sem mudança.

Valor de retorno — reescrever os argumentos da ferramenta:

return {"action": "modify", "args": {"new_string": "conteúdo corrigido"}}

O dicionário args devolvido é mesclado de forma rasa sobre os argumentos originais da ferramenta antes de ela executar. Vários hooks modify se acumulam — as chaves de cada hook são mescladas num único dicionário acumulado, construído a partir dos argumentos originais, de modo que o hook A mudando path e o hook B mudando content sobrevivem os dois. Se dois hooks alteram a mesma chave, vence o hook mais tardio.

Os hooks de shell também aceitam o formato compatível com o Claude Code:

{"decision": "modify", "tool_input": {"new_string": "fixed content"}}

Os dois formatos são normalizados internamente para {"action": "modify", "args": {...}}.

Se um callback de pre_tool_call passa de plugins.hook_callback_timeout (ou ainda está rodando de um disparo anterior que estourou o tempo), o Hermes falha fechado: a ferramenta é bloqueada com uma mensagem de tempo esgotado, em vez de seguir sem decisão de política.

Para que serve: registro em log, trilhas de auditoria, contadores de chamada de ferramenta, bloqueio de operações perigosas, limitação de taxa, aplicação de política por usuário, saneamento de argumentos, reescrita de caminhos, injeção de parâmetros padrão.

Exemplo — log de auditoria de chamadas de ferramenta:

import json, logging
from datetime import datetime

logger = logging.getLogger(__name__)

def audit_tool_call(tool_name, args, task_id, **kwargs):
logger.info("TOOL_CALL session=%s tool=%s args=%s",
task_id, tool_name, json.dumps(args)[:200])

def register(ctx):
ctx.register_hook("pre_tool_call", audit_tool_call)

Exemplo — avisar em ferramentas perigosas:

DANGEROUS = {"terminal", "write_file", "patch"}

def warn_dangerous(tool_name, **kwargs):
if tool_name in DANGEROUS:
print(f"⚠ Executando ferramenta potencialmente perigosa: {tool_name}")

def register(ctx):
ctx.register_hook("pre_tool_call", warn_dangerous)

post_tool_call​

Dispara imediatamente depois que toda execução de ferramenta retorna.

Assinatura do callback:

def my_callback(tool_name: str, args: dict, result: str, task_id: str,
duration_ms: int, **kwargs):
ParâmetroTipoDescrição
tool_namestrNome da ferramenta que acabou de executar
argsdictOs argumentos que o modelo passou para a ferramenta
resultstrO valor de retorno da ferramenta (sempre uma string JSON)
task_idstrIdentificador de sessão/tarefa. String vazia se não estiver definido.
duration_msintQuanto tempo levou o despacho da ferramenta, em milissegundos (medido com time.monotonic() em volta de registry.dispatch()).

Dispara: em model_tools.py, dentro de handle_function_call(), depois que o handler da ferramenta retorna. Dispara uma vez por chamada de ferramenta. Não dispara se a ferramenta levantou uma exceção não tratada (o erro é capturado e devolvido como string JSON de erro, e o post_tool_call dispara com essa string de erro em result).

Valor de retorno: ignorado.

Para que serve: registrar resultados de ferramenta, coletar métricas, acompanhar taxas de sucesso/falha por ferramenta, painéis de latência, alertas de orçamento por ferramenta, mandar notificações quando ferramentas específicas terminam.

Exemplo — acompanhar métricas de uso de ferramentas:

from collections import Counter, defaultdict
import json

_tool_counts = Counter()
_error_counts = Counter()
_latency_ms = defaultdict(list)

def track_metrics(tool_name, result, duration_ms=0, **kwargs):
_tool_counts[tool_name] += 1
_latency_ms[tool_name].append(duration_ms)
try:
parsed = json.loads(result)
if "error" in parsed:
_error_counts[tool_name] += 1
except (json.JSONDecodeError, TypeError):
pass

def register(ctx):
ctx.register_hook("post_tool_call", track_metrics)

pre_llm_call​

Dispara uma vez por turno, antes de o laço de chamada de ferramentas começar. Todos os retornos válidos dos callbacks são agregados na ordem dos plugins e injetados na mensagem do usuário do turno atual.

Assinatura do callback:

def my_callback(session_id: str, user_message: str, conversation_history: list,
is_first_turn: bool, model: str, platform: str, **kwargs):
ParâmetroTipoDescrição
session_idstrIdentificador único da sessão atual
user_messagestrA mensagem original do usuário neste turno (antes de qualquer injeção de skill)
conversation_historylistCópia da lista completa de mensagens (formato OpenAI: [{"role": "user", "content": "..."}])
is_first_turnboolTrue se este é o primeiro turno de uma sessão nova, False nos turnos seguintes
modelstrO identificador do modelo (por exemplo "anthropic/claude-sonnet-4.6")
platformstrOnde a sessão está rodando: "cli", "telegram", "discord" etc.

Dispara: em run_agent.py, dentro de run_conversation(), depois da compressão de contexto, mas antes do laço while principal. Dispara uma vez por chamada de run_conversation() (ou seja, uma vez por turno do usuário), não uma vez por chamada de API dentro do laço de ferramentas.

Valor de retorno: se o callback devolve um dicionário com a chave "context", ou uma string simples não vazia, o texto é anexado à mensagem do usuário do turno atual. Devolva None para não injetar nada.

# Injeta contexto
return {"context": "Memórias recuperadas:\n- O usuário gosta de Python\n- Está trabalhando no hermes-agent"}

# String simples (equivalente)
return "Memórias recuperadas:\n- O usuário gosta de Python"

# Sem injeção
return None

Onde o contexto é injetado: sempre na mensagem do usuário, nunca no prompt do sistema. Isso preserva o cache de prompt — o prompt do sistema continua idêntico entre turnos, então os tokens em cache são reaproveitados. O prompt do sistema é território do Hermes (orientação ao modelo, imposição de ferramentas, personalidade, skills). Os plugins contribuem com contexto ao lado da entrada do usuário.

O content limpo da mensagem do usuário continua inalterado. Para reprodução e estabilidade do cache de prompt, o Hermes pode persistir a mensagem exata enviada à API, incluindo o contexto injetado pelo plugin, no campo lateral api_content da linha.

Quando vários plugins devolvem contexto, as saídas deles são unidas por linhas em branco duplas na ordem de descoberta dos plugins (alfabética pelo nome do diretório).

Para que serve: recuperação de memória, injeção de contexto RAG, salvaguardas, análise por turno.

Exemplo — recuperação de memória:

import httpx

MEMORY_API = "https://sua-api-de-memoria.example.com"

def recall(session_id, user_message, is_first_turn, **kwargs):
try:
resp = httpx.post(f"{MEMORY_API}/recall", json={
"session_id": session_id,
"query": user_message,
}, timeout=3)
memories = resp.json().get("results", [])
if not memories:
return None
text = "Contexto recuperado:\n" + "\n".join(f"- {m['text']}" for m in memories)
return {"context": text}
except Exception:
return None

def register(ctx):
ctx.register_hook("pre_llm_call", recall)

Exemplo — salvaguardas:

POLICY = "Nunca execute comandos que apaguem arquivos sem confirmação explícita do usuário."

def guardrails(**kwargs):
return {"context": POLICY}

def register(ctx):
ctx.register_hook("pre_llm_call", guardrails)

post_llm_call​

Dispara uma vez por turno, depois que o laço de chamada de ferramentas termina e o agente produziu uma resposta final. Só dispara em turnos bem-sucedidos — não dispara se o turno foi interrompido.

Assinatura do callback:

def my_callback(session_id: str, user_message: str, assistant_response: str,
conversation_history: list, model: str, platform: str, **kwargs):
ParâmetroTipoDescrição
session_idstrIdentificador único da sessão atual
user_messagestrA mensagem original do usuário neste turno
assistant_responsestrA resposta final em texto do agente neste turno
conversation_historylistCópia da lista completa de mensagens depois que o turno terminou
modelstrO identificador do modelo
platformstrOnde a sessão está rodando

Dispara: em run_agent.py, dentro de run_conversation(), depois que o laço de ferramentas sai com uma resposta final. Protegido por if final_response and not interrupted — ou seja, não dispara quando o usuário interrompe no meio do turno ou quando o agente bate no limite de iterações sem produzir resposta.

Valor de retorno: ignorado.

Para que serve: sincronizar dados da conversa com um sistema de memória externo, calcular métricas de qualidade de resposta, registrar resumos de turno, disparar ações de acompanhamento.

Exemplo — sincronizar com memória externa:

import httpx

MEMORY_API = "https://sua-api-de-memoria.example.com"

def sync_memory(session_id, user_message, assistant_response, **kwargs):
try:
httpx.post(f"{MEMORY_API}/store", json={
"session_id": session_id,
"user": user_message,
"assistant": assistant_response,
}, timeout=5)
except Exception:
pass # melhor esforço

def register(ctx):
ctx.register_hook("post_llm_call", sync_memory)

Exemplo — acompanhar o tamanho das respostas:

import logging
logger = logging.getLogger(__name__)

def log_response_length(session_id, assistant_response, model, **kwargs):
logger.info("RESPONSE session=%s model=%s chars=%d",
session_id, model, len(assistant_response or ""))

def register(ctx):
ctx.register_hook("post_llm_call", log_response_length)

pre_verify​

Dispara uma vez por turno em que o agente editou código, pouco antes de ele terminar (depois da proteção embutida de verificar-ao-parar). Este é um portão de política do usuário ou do plugin: um callback pode manter o agente andando — rodar uma checagem, adiá-la, arrumar o diff — em vez de deixar ele parar.

A orientação de verificação que o Hermes traz não é um hook pre_verify padrão. Ela é acrescentada ao empurrãozinho de verificar-ao-parar baseado em evidências quando o código editado não tem evidência recente de verificação, então não cria um segundo caminho padrão de continuação. Defina agent.verify_guidance: false para deixar esse empurrãozinho embutido bem curto.

Assinatura do callback:

def my_callback(session_id: str, platform: str, model: str, coding: bool,
attempt: int, final_response: str, changed_paths: list, **kwargs):
ParâmetroTipoDescrição
session_idstrIdentificador único da sessão atual
platformstrOnde a sessão está rodando ("cli", "telegram", …)
codingboolSe o turno está na postura de programação (num espaço de trabalho de código) — use isso para limitar o escopo do seu hook
modelstrO identificador do modelo
attemptintQuantas vezes este turno já levou empurrãozinho (0 na primeira) — use isso para se autolimitar
final_responsestrA resposta que o agente está prestes a entregar
changed_pathslistArquivos que o agente editou neste turno (ordenados; aqui, nunca vazio)

Restrinja um hook ao contexto de programação checando coding, e faça-o de disparo único com attempt (hooks de shell leem os dois em .extra), do mesmo jeito que um hook pre_tool_call se restringe por tool_name — assim você pode registrar vários hooks pre_verify, cada um disparando só onde deve.

Dispara: em agent/conversation_loop.py, no ponto em que o agente aceitaria uma resposta final, logo depois da checagem de verificar-ao-parar — mas só quando o agente editou código naquele turno e há pelo menos um hook pre_verify registrado.

Valor de retorno — manter o agente andando:

return {"action": "continue", "message": "Rode o formatador nas suas mudanças e então termine."}

A message é anexada como um turno de usuário sintético e o laço roda de novo. O formato Stop do Claude Code ({"decision": "block", "reason": "..."}, em que bloquear a parada significa continuar) também é aceito. Uma diretiva sem mensagem — ou qualquer outro retorno — deixa o turno terminar.

Com limite: diretivas de continuar consecutivas num mesmo turno são limitadas por agent.max_verify_nudges (padrão 3), então um hook que sempre manda continuar nunca consegue prender o laço. A resposta tentada fica no histórico, mas não é mostrada ao usuário enquanto o agente está levando empurrãozinho.

Faça o hook ser idempotente: ele dispara de novo depois de cada empurrãozinho, então filtre por attempt (if attempt: return None) — senão ele só fica empurrando até bater no limite.

Para que serve: adiar testes/lints durante iteração criativa, exigir checagens verdes para certos caminhos, bloquear o "pronto" até existir uma entrada no changelog, rodar uma checklist de verificação específica do projeto.

Exemplo — adiar checagens em trabalho criativo de UI, com escopo e disparo único:

UI = (".tsx", ".jsx", ".css", ".scss")

def defer_ui_checks(coding, attempt, changed_paths, **kwargs):
if attempt or not coding:
return None # disparo único, só em programação
if not all(p.endswith(UI) for p in changed_paths):
return None # só edições puramente de UI
return {
"action": "continue",
"message": "Isto é trabalho de UI — não rode testes/lints ainda; peça ao usuário "
"para dar uma olhada primeiro, e limpe o diff antes de qualquer commit.",
}

def register(ctx):
ctx.register_hook("pre_verify", defer_ui_checks)

Para orientação permanente que deve moldar o empurrãozinho embutido de falta de evidência, use agent.verify_guidance. Para regras mais amplas de postura de programação, que não precisam barrar a verificação, prefira agent.coding_instructions no config.yaml — ela pega carona no briefing de programação e não custa um turno a mais.


transform_api_error_classification​

Dispara uma vez por chamada de API que falha, no topo de agent/error_classifier.classify_api_error(), antes do pipeline embutido. Plugins de provedor usam esse hook para assumir as manias de erro do próprio provedor sem remendar o núcleo. Ele muda comportamento (família de transformação): a classificação devolvida governa nova tentativa, compressão, rotação de credenciais e roteamento de reserva.

Os callbacks recebem o contexto de erro já interpretado como argumentos nomeados — provider (restrinja o escopo por ele), model, status_code, error_type, error_code, error_message, error_body, error, approx_tokens, context_length, num_messages. Devolva None para recusar, ou um dicionário para assumir o erro:

return {"reason": "model_not_found", # obrigatório: um nome de FailoverReason
"retryable": False, "should_fallback": True} # sobrescritas opcionais de dica de recuperação

O despacho é roda-todos-depois-escolhe-o-primeiro: todo callback roda, as falhas são isoladas, e vence o primeiro resultado válido na ordem de registro (resultados válidos que perderam geram um aviso em tempo de execução). Dicionários inválidos e motivos desconhecidos são pulados, então um plugin quebrado nunca consegue quebrar a classificação.

Privacidade: error_message e error_body podem carregar dados do provedor sem redação. Só plugins Python — registros por shell são recusados na leitura da configuração, com um aviso.


on_session_start​

Dispara uma vez quando uma sessão totalmente nova é criada. Não dispara na continuação de sessão (quando o usuário manda uma segunda mensagem numa sessão que já existe).

Assinatura do callback:

def my_callback(session_id: str, model: str, platform: str, **kwargs):
ParâmetroTipoDescrição
session_idstrIdentificador único da sessão nova
modelstrO identificador do modelo
platformstrOnde a sessão está rodando

Dispara: em run_agent.py, dentro de run_conversation(), durante o primeiro turno de uma sessão nova — especificamente depois de o prompt do sistema ser montado, mas antes de o laço de ferramentas começar. A checagem é if not conversation_history (sem mensagens anteriores = sessão nova).

Valor de retorno: ignorado.

Para que serve: inicializar estado com escopo de sessão, aquecer caches, registrar a sessão num serviço externo, registrar em log os inícios de sessão.

Exemplo — inicializar um cache de sessão:

_session_caches = {}

def init_session(session_id, model, platform, **kwargs):
_session_caches[session_id] = {
"model": model,
"platform": platform,
"tool_calls": 0,
"started": __import__("datetime").datetime.now().isoformat(),
}

def register(ctx):
ctx.register_hook("on_session_start", init_session)

on_session_end​

Dispara bem no fim de toda chamada de run_conversation(), qualquer que seja o desfecho. Também dispara pelo tratador de saída da CLI, se o agente estava no meio de um turno quando o usuário saiu.

Assinatura do callback:

def my_callback(session_id: str, completed: bool, interrupted: bool,
model: str, platform: str, **kwargs):
ParâmetroTipoDescrição
session_idstrIdentificador único da sessão
completedboolTrue se o agente produziu uma resposta final, False caso contrário
interruptedboolTrue se o turno foi interrompido (usuário mandou mensagem nova, /stop, ou saiu)
modelstrO identificador do modelo
platformstrOnde a sessão está rodando

Dispara: em dois lugares:

  1. run_agent.py — no fim de toda chamada de run_conversation(), depois de toda a limpeza. Sempre dispara, mesmo se o turno deu erro.
  2. cli.py — no tratador atexit da CLI, mas só se o agente estava no meio de um turno (_agent_running=True) quando a saída aconteceu. Isso pega Ctrl+C e /exit durante o processamento. Nesse caso, completed=False e interrupted=True.

Valor de retorno: ignorado.

Para que serve: descarregar buffers, fechar conexões, persistir o estado da sessão, registrar a duração da sessão, limpar recursos criados no on_session_start.

Exemplo — descarregar e limpar:

_session_caches = {}

def cleanup_session(session_id, completed, interrupted, **kwargs):
cache = _session_caches.pop(session_id, None)
if cache:
# Descarrega os dados acumulados em disco ou num serviço externo
status = "completed" if completed else ("interrupted" if interrupted else "failed")
print(f"Sessão {session_id} encerrada: {status}, {cache['tool_calls']} chamadas de ferramenta")

def register(ctx):
ctx.register_hook("on_session_end", cleanup_session)

Exemplo — acompanhar a duração da sessão:

import time, logging
logger = logging.getLogger(__name__)

_start_times = {}

def on_start(session_id, **kwargs):
_start_times[session_id] = time.time()

def on_end(session_id, completed, interrupted, **kwargs):
start = _start_times.pop(session_id, None)
if start:
duration = time.time() - start
logger.info("SESSION_DURATION session=%s seconds=%.1f completed=%s interrupted=%s",
session_id, duration, completed, interrupted)

def register(ctx):
ctx.register_hook("on_session_start", on_start)
ctx.register_hook("on_session_end", on_end)

on_session_finalize​

Dispara quando a CLI ou o gateway desmonta uma sessão ativa — por exemplo, quando o usuário roda /new, quando o gateway recolhe uma sessão ociosa, ou quando a CLI sai com um agente ativo. Use para descarregar estado atrelado ao ID da sessão que está saindo. No reset do gateway, a sessão substituta já existe antes de este callback rodar.

Assinatura do callback:

def my_callback(session_id: str | None, platform: str, **kwargs):
ParâmetroTipoDescrição
session_idstr ou NoneO ID da sessão que está saindo. Pode ser None se não havia sessão ativa.
platformstr"cli" ou o nome da plataforma de mensagens ("telegram", "discord" etc.).

Dispara: na desmontagem de CLI/TUI e nos caminhos de reset, desligamento ou expiração por ociosidade do gateway. Desligamento e expiração do gateway podem finalizar sem um on_session_reset correspondente.

Valor de retorno: ignorado.

Para que serve: persistir métricas finais da sessão antes de o ID ser descartado, fechar recursos por sessão, emitir um evento final de telemetria, drenar escritas na fila.


on_session_reset​

Dispara numa fronteira de sessão da CLI ou da TUI, ou quando o gateway troca por uma chave de sessão nova num chat ativo. Isso deixa os plugins reagirem ao estado de conversa zerado sem esperar o próximo on_session_start.

Assinatura do callback:

def my_callback(session_id: str, platform: str, **kwargs):
ParâmetroTipoDescrição
session_idstrO ID da sessão nova (já rotacionado para o valor novo).
platformstr"cli", "tui", ou o nome da plataforma de mensagens.
reasonstr, opcionalPresente nos caminhos de reset da CLI e do gateway.
old_session_idstr, opcionalSó no gateway: o ID da sessão que está saindo.
new_session_idstr, opcionalSó no gateway: o ID da sessão substituta.

Dispara: a CLI fornece session_id, platform e reason; a TUI fornece session_id e platform; o gateway acrescenta reason, old_session_id e new_session_id depois de alocar a chave substituta. No reset do gateway, a ordem é: criar e persistir a substituta → on_session_finalize(old_id) → on_session_reset(new_id) → on_session_start(new_id) no primeiro turno de entrada.

Valor de retorno: ignorado.

Para que serve: zerar caches por sessão indexados por session_id, emitir análises de "sessão rotacionada", preparar um balde de estado novo.


Veja o guia Criar um Plugin para o passo a passo completo, incluindo esquemas de ferramenta, handlers e padrões avançados de hook.


subagent_start​

Dispara uma vez por agente filho, depois que o delegate_task construiu o AIAgent filho e antes de esse filho rodar. Você delegando uma tarefa só ou um lote de três, este hook dispara uma vez para cada filho.

Este hook é específico do ciclo de vida de delegação/subagente. Ele não é um portão universal de "antes de qualquer invocação de agente" para execuções de agente originadas de gateway, CLI, cron, lote, MoA ou outros executores.

Assinatura do callback:

def my_callback(parent_session_id: str | None,
parent_turn_id: str,
parent_subagent_id: str | None,
child_session_id: str | None,
child_subagent_id: str,
child_role: str,
child_goal: str,
**kwargs):
ParâmetroTipoDescrição
parent_session_idstr | NoneID de sessão do agente pai que delegou.
parent_turn_idstrID do turno do agente pai que pediu a delegação, se disponível.
parent_subagent_idstr | NoneID de subagente do pai, quando este filho foi criado por outro subagente; None para agentes pais de topo.
child_session_idstr | NoneID de sessão alocado para o agente filho.
child_subagent_idstrID estável de subagente usado pela observabilidade e pelos controles de delegação.
child_rolestrPapel efetivo do filho depois de aplicada a política de delegação, por exemplo "leaf" ou "orchestrator".
child_goalstrObjetivo/prompt delegado que o agente filho vai executar.

Dispara: em tools/delegate_tool.py, dentro de _build_child_agent(), depois que o AIAgent filho foi construído e anotado com os metadados de identidade de subagente, e antes de o _run_single_child() rodar o filho.

Valor de retorno: ignorado. Este é só um hook observador; devolver um valor não bloqueia nem altera a execução do agente filho.

Para que serve: registrar a criação de subagentes, mapear as relações de sessão pai/filho, acompanhar árvores de delegação aninhadas, emitir registros de auditoria antes da execução, pré-alocar recursos de observabilidade por filho.

Exemplo — registrar a criação de subagentes:

import logging

logger = logging.getLogger(__name__)

def log_subagent_start(
parent_session_id,
parent_turn_id,
child_session_id,
child_subagent_id,
child_role,
child_goal,
**kwargs,
):
logger.info(
"SUBAGENT_START parent=%s turn=%s child_session=%s child=%s role=%s goal=%r",
parent_session_id,
parent_turn_id,
child_session_id,
child_subagent_id,
child_role,
child_goal[:200],
)

def register(ctx):
ctx.register_hook("subagent_start", log_subagent_start)
informação

O subagent_start é útil para observabilidade de delegação, mas não é um hook de política bloqueante. Para bloquear a delegação antes de um filho ser construído, use o pre_tool_call para bloquear a chamada da ferramenta delegate_task.


subagent_stop​

Dispara uma vez por agente filho, depois que o delegate_task termina. Você tendo delegado uma tarefa só ou um lote de três, este hook dispara uma vez para cada filho. O despacho é serializado na thread do pai depois que os futuros dos filhos drenam, e o corpo de cada callback Python roda nessa mesma thread chamadora (não num worker com limite de tempo).

Assinatura do callback:

def my_callback(parent_session_id: str, child_role: str | None,
child_summary: str | None, child_status: str,
tool_call_history: list[dict], duration_ms: int, **kwargs):
ParâmetroTipoDescrição
parent_session_idstrID de sessão do agente pai que delegou
child_rolestr | NoneEtiqueta de papel de orquestrador definida no filho (None se o recurso não está ligado)
child_summarystr | NoneA resposta final que o filho devolveu ao pai
child_statusstr"completed", "failed", "interrupted" ou "error"
tool_call_historylist[dict]Chamadas de ferramenta em ordem, só com metadados: tool_name, tool_input limitado, input_bytes, output_bytes e status; entradas e saídas cruas ficam de fora
duration_msintTempo de relógio gasto rodando o filho, em milissegundos

Dispara: em tools/delegate_tool.py, depois que o ThreadPoolExecutor.as_completed() drena todos os futuros dos filhos. O invoke_hook("subagent_stop", ...) é encaminhado para a thread do pai, para quem escreve o hook não esbarrar em reentrância do pool de filhos, e os callbacks ficam nessa thread chamadora.

Valor de retorno: ignorado.

Para que serve: registrar a atividade de orquestração, acumular durações dos filhos para cobrança, escrever registros de auditoria pós-delegação.

Exemplo — registrar a atividade do orquestrador:

import logging
logger = logging.getLogger(__name__)

def log_subagent(parent_session_id, child_role, child_status, duration_ms, **kwargs):
logger.info(
"SUBAGENT parent=%s role=%s status=%s duration_ms=%d",
parent_session_id, child_role, child_status, duration_ms,
)

def register(ctx):
ctx.register_hook("subagent_stop", log_subagent)
informação

Com delegação pesada (por exemplo, papéis de orquestrador × 5 folhas × profundidade aninhada), o subagent_stop dispara muitas vezes por turno. Mantenha o seu callback rápido; empurre o trabalho caro para uma fila em segundo plano.


pre_gateway_dispatch​

Dispara uma vez por MessageEvent recebido no gateway, depois da proteção de eventos internos, mas antes da autenticação/pareamento e do despacho ao agente. Este é o ponto de interceptação para políticas de fluxo de mensagens no nível do gateway (janelas de só escutar, passagem para humano, roteamento por chat etc.) que não cabem direito em nenhum adaptador de plataforma específico.

Assinatura do callback:

def my_callback(event, gateway, session_store, **kwargs):
ParâmetroTipoDescrição
eventMessageEventA mensagem de entrada normalizada (tem .text, .source, .message_id, .internal etc.).
gatewayGatewayRunnerO executor de gateway ativo, para os plugins poderem chamar gateway.adapters[platform].send(...) e mandar respostas por canal lateral (avisos ao dono etc.).
session_storeSessionStorePara ingestão silenciosa na transcrição, via session_store.append_to_transcript(...).

Dispara: em gateway/run.py, dentro de GatewayRunner._handle_message(), logo depois de o is_internal ser calculado. Eventos internos pulam o hook por completo (são gerados pelo sistema — conclusões de processo em segundo plano etc. — e não podem ser barrados por política voltada ao usuário).

Valor de retorno: None ou um dicionário. Vence o primeiro dicionário de ação reconhecido; os resultados dos demais plugins são ignorados. Exceções nos callbacks de plugin são capturadas e registradas; em caso de erro, o gateway sempre segue para o despacho normal.

RetornoEfeito
{"action": "skip", "reason": "..."}Descarta a mensagem — sem resposta do agente, sem fluxo de pareamento, sem autenticação. Presume-se que o plugin cuidou dela (por exemplo, ingerindo em silêncio na transcrição).
{"action": "rewrite", "text": "novo texto"}Substitui o event.text e segue com o despacho normal, usando o evento modificado. Útil para juntar mensagens ambientes acumuladas num único prompt.
{"action": "allow"} / NoneDespacho normal — roda a cadeia completa de autenticação / pareamento / laço do agente.

Para que serve: grupos de só escutar (só responder quando marcado; acumular mensagens ambientes no contexto); passagem para humano (ingerir em silêncio as mensagens do cliente enquanto o dono cuida do chat na mão); limitação de taxa por perfil; roteamento guiado por política.

Exemplo — descartar DMs não autorizadas em silêncio, sem acionar o código de pareamento:

def deny_unauthorized_dms(event, **kwargs):
src = event.source
if src.chat_type == "dm" and not _is_approved_user(src.user_id):
return {"action": "skip", "reason": "unauthorized-dm"}
return None

def register(ctx):
ctx.register_hook("pre_gateway_dispatch", deny_unauthorized_dms)

Exemplo — transformar um buffer de mensagens ambientes num único prompt ao ser mencionado:

_buffers = {}

def buffer_or_rewrite(event, **kwargs):
key = (event.source.platform, event.source.chat_id)
buf = _buffers.setdefault(key, [])
if _bot_mentioned(event.text):
combined = "\n".join(buf + [event.text])
buf.clear()
return {"action": "rewrite", "text": combined}
buf.append(event.text)
return {"action": "skip", "reason": "ambient-buffered"}

def register(ctx):
ctx.register_hook("pre_gateway_dispatch", buffer_or_rewrite)

gateway_platform_event​

Dispara para eventos nativos de plataforma suportados somente depois que a checagem normal de autorização do gateway, com escopo de perfil, passa. O callback recebe dicionários simples; objetos crus do SDK, handles de adaptador, clientes de bot e contextos de callback nunca fazem parte deste contrato estável.

As reações a mensagens do Telegram foram o primeiro evento suportado; edições e exclusões de mensagem e eventos de ciclo de vida de thread vieram depois:

def on_platform_event(platform, event_type, payload, **kwargs):
if platform == "telegram" and event_type == "reaction":
print(payload["chat_id"], payload["message_id"], payload["emojis"])
elif event_type == "message_edited":
print(platform, payload["chat_id"], payload["message_id"], payload["text"])

def register(ctx):
ctx.register_hook("gateway_platform_event", on_platform_event)
ParâmetroTipoDescrição
platformstrId estável da plataforma ("telegram", "discord").
event_typestrId de contrato local do evento (veja a tabela abaixo).
payloaddictCampos específicos do tipo de evento, documentados por tipo abaixo.

Todo payload é aditivo e específico do evento; não existe uma versão monolítica de payload de gateway. Todos os ids são strings; campos ausentes ou indisponíveis vêm como None, nunca adivinhados. Eventos malformados e eventos cuja origem não pode ser autorizada são descartados (falha fechada). Uma reconstrução transitória da Application do Telegram registra o observador de novo, junto com os handlers principais.

Contratos de payload por evento (v1, aditivos):

event_typePlataformasCampos do payload
reactiontelegramemojis: list[str], custom_emoji_ids: list[str], chat_id: str, message_id: str, thread_id: str | None (as atualizações de reação do Telegram não trazem id de tópico, então hoje é sempre None).
message_editedtelegram, discordchat_id: str, message_id: str, thread_id: str | None, text: str | None (texto ou legenda editados, limitados; None para edições só de mídia ou quando não está em cache), edited_at: str | None (ISO 8601).
message_deleteddiscordchat_id: str, message_id: str, thread_id: str | None, author_id: str | None. O evento de exclusão do Discord não identifica quem apagou; a origem autorizada é o autor da mensagem apagada, e exclusões fora do cache nunca disparam.
thread_createddiscordthread_id: str, parent_chat_id: str | None, name: str | None, owner_id: str | None.
thread_renameddiscordthread_id: str, parent_chat_id: str | None, old_name: str | None, new_name: str. Disparado só quando o nome mudou de fato; outras atualizações de thread (arquivamento, modo lento, etiquetas) são descartadas. O evento de atualização de thread do Discord não traz o ator, então o dono da thread é a origem autorizada.

As edições progressivas que o próprio bot faz nas mensagens (streaming) nunca disparam message_edited no Discord — eventos gerados pelo bot são descartados no ponto de disparo.

Este hook é só de observação: ele não dá acesso ao evento cru nem ao adaptador. O acesso ao payload cru do SDK foi deliberadamente deixado de fora — objetos de SDK de adaptador mudam de forma sem aviso e virariam superfície de API impossível de evoluir; onde for realmente necessário, isso exige uma capacidade explícita própria (gateway.raw_events), com o rótulo de "sem garantia de estabilidade" e um projeto próprio (acompanhado em #64228). Para agir numa plataforma (adicionar uma reação, renomear uma thread), use a fachada ctx.platform_actions, protegida por capacidade e documentada no guia de plugins — ela vem desligada por padrão, atrás da capacidade gateway.platform_actions. O PluginContext.dispatch_tool() só consegue chamar ferramentas registradas no registro de ferramentas; o send_message de propósito não está registrado lá (o transporte dele é reservado para os caminhos explícitos de entrega da CLI, do cron, do kanban e do MCP). Um futuro contrato de entrega de saída precisa primeiro fornecer conteúdo e handles de entrega estáveis em todos os adaptadores; esta fatia não pré-registra um hook gateway_message_delivered inerte.


pre_approval_request​

Dispara antes de uma decisão de aprovação ser solicitada. Cobre as superfícies com pedido — CLI interativa, TUI Ink, plataformas de gateway e clientes ACP — e as decisões de approvals.mode=smart tomadas sem pedido humano (surface="smart"). No modo inteligente, o hook roda antes de o LLM auxiliar ser chamado.

Este é o lugar certo para ligar um notificador próprio — por exemplo, um app de barra de menu no macOS que abre uma notificação de permitir/negar, ou um log de auditoria que registra cada pedido de aprovação com contexto.

Assinatura do callback:

def my_callback(
command: str,
description: str,
pattern_key: str,
pattern_keys: list[str],
session_key: str,
surface: str,
**kwargs,
):
ParâmetroTipoDescrição
commandstrComando de terminal ou script do execute_code sendo avaliado. Payloads inteligentes e de gateway são redigidos antes do despacho ao observador. A redação do observador inteligente é obrigatória mesmo quando security.redact_secrets está desligado; se a redação falha, os hooks inteligentes são pulados.
descriptionstrMotivo(s) legível(is) pelos quais o comando foi sinalizado (combinados quando vários padrões batem)
pattern_keystrChave do padrão principal que acionou a aprovação (por exemplo "rm_rf", "sudo")
pattern_keyslist[str]Todas as chaves de padrão que bateram
session_keystrIdentificador de sessão, útil para restringir notificações por chat
surfacestr"cli" para pedidos interativos na CLI/TUI, "gateway" para aprovações assíncronas de plataforma, ou "smart" para decisões automáticas de aprovar/negar pelo LLM auxiliar

Valor de retorno: ignorado. Os hooks aqui são só de observação; eles não podem vetar nem antecipar a aprovação. Use o pre_tool_call para bloquear uma ferramenta antes que ela chegue ao sistema de aprovação.

Para que serve: notificações de desktop, alertas push, log de auditoria, webhooks do Slack, roteamento de escalonamento, métricas.

Exemplo — notificação de desktop no macOS:

import subprocess

def notify_approval(command, description, session_key, **kwargs):
title = "O Hermes precisa de aprovação"
body = f"{description}: {command[:80]}"
subprocess.Popen([
"osascript", "-e",
f'display notification "{body}" with title "{title}"',
])

def register(ctx):
ctx.register_hook("pre_approval_request", notify_approval)

post_approval_response​

Dispara depois de uma decisão de aprovação pedida ou inteligente, depois que um pedido expira, ou quando o gateway não consegue entregar a notificação de aprovação. A falha de notificação emite choice="notify_failed" antes de existir qualquer decisão de aprovação.

Assinatura do callback:

def my_callback(
command: str,
description: str,
pattern_key: str,
pattern_keys: list[str],
session_key: str,
surface: str,
choice: str,
**kwargs,
):

Os mesmos argumentos nomeados do pre_approval_request, mais:

ParâmetroTipoDescrição
choicestrAs superfícies com pedido usam "once", "session", "always", "deny", "timeout" ou "notify_failed"; decisões inteligentes usam "smart_approve" ou "smart_deny"
decided_bystr"aux_llm" nas decisões inteligentes; ausente nas superfícies com pedido

Valor de retorno: ignorado.

Para que serve: fechar a notificação de desktop correspondente, registrar a decisão final num log de auditoria, atualizar métricas, avançar um limitador de taxa.

def log_decision(command, choice, session_key, **kwargs):
logger.info("approval %s: %s for session %s", choice, command[:60], session_key)

def register(ctx):
ctx.register_hook("post_approval_response", log_decision)

pre_transcription​

Dispara dentro do despachante de STT (tools.transcription_tools.transcribe_audio) depois que o provedor foi resolvido e antes de qualquer backend ser invocado, seja ele embutido, um provedor type: command ou um provedor registrado por plugin. Deixa o plugin conduzir a própria requisição de transcrição, em vez de só observar a transcrição depois.

Assinatura do callback:

def my_callback(
file_path: str,
provider: str,
model: str | None,
language: str | None,
prompt: str | None,
source: str | None,
**kwargs,
) -> dict | None:
ParâmetroTipoDescrição
file_pathstrCaminho absoluto do arquivo de áudio prestes a ser transcrito. Somente leitura.
providerstrProvedor de STT resolvido (local, groq, openai, mistral, xai, elevenlabs, deepinfra, local_command, o nome de um provedor de comando, ou o nome de um provedor de plugin).
modelstr | NoneModelo resolvido até aqui, ou None quando vale o padrão do backend.
languagestr | NoneIdioma vindo da seção de configuração do provedor, ou None.
promptstr | NoneO valor estático de stt.prompt, ou None.
sourcestr | NoneRótulo da superfície chamadora (gateway, voice_mode, …). Só para observabilidade, não é usado no despacho.

Valor de retorno: um dict com qualquer combinação de "prompt", "language" e "model" mapeados para strings, ou None para deixar a requisição intacta. Valores que não sejam string, chaves desconhecidas e file_path são ignorados (tentativas de mexer em file_path são registradas como aviso). Os resultados são aplicados na ordem de registro, valendo quem escreve por último em cada campo, por cima do valor de stt.prompt da configuração. Devolver "" em prompt limpa o prompt configurado para aquela requisição.

Para que serve: injetar uma lista de vocabulário por usuário ou por chat antes de o áudio ser enviado, forçar o language a partir da localidade de quem chama, rebaixar o model em gravações longas, rotear fontes ruidosas para outro modelo.

VOCAB = "Hermes, Teknium, Nous Research, kanban"

def add_vocab(provider, prompt, source, **kwargs):
if source != "gateway":
return None
return {"prompt": f"{prompt}. {VOCAB}" if prompt else VOCAB}

def register(ctx):
ctx.register_hook("pre_transcription", add_vocab)

Nem todo backend aceita um prompt. O local mapeia o prompt para o initial_prompt do faster-whisper; openai, groq, mistral e deepinfra mandam como prompt; xai, elevenlabs, local_command e provedores type: command registram em DEBUG e transcrevem sem ele. Veja a tabela de suporte por provedor para a matriz completa e o limite de privacidade. Erros de encanamento do hook falham abertos: o despacho continua com a requisição sem modificação.


transform_tool_result​

Dispara depois que uma ferramenta retorna e antes de o resultado ser anexado à conversa. Deixa um plugin reescrever a string de resultado de QUALQUER ferramenta — não só a saída do terminal — antes de o modelo ver.

Assinatura do callback:

def my_callback(tool_name: str, args: dict, result: str, task_id: str, **kwargs) -> str | None:

O payload completo também inclui session_id, tool_call_id, turn_id, api_request_id, duration_ms, status, error_type e error_message. O result é o resultado final devolvido pelo despacho da ferramenta; ele e o args podem conter conteúdo arbitrário de ferramenta ou do usuário, além de segredos.

Valor de retorno: a primeira str substitui o resultado (inclusive uma string vazia); None deixa como está.

Para que serve: redigir dados pessoais específicos da organização na saída do web_extract, embrulhar respostas JSON longas de ferramenta num cabeçalho de resumo, injetar dicas de recuperação nos resultados do read_file, reescrever relatórios de subagente do delegate_task num esquema específico do projeto.

import re
SECRET = re.compile(r"sk-[A-Za-z0-9]{32,}")

def redact_secrets(tool_name, result, **kwargs):
if SECRET.search(result):
return SECRET.sub("[REDACTED]", result)
return None

def register(ctx):
ctx.register_hook("transform_tool_result", redact_secrets)

Vale para toda ferramenta. Para reescrever só o terminal, veja o transform_terminal_output abaixo — ele é mais estreito, roda antes do transform_tool_result, e a substituição dele ainda está sujeita ao limite final de saída da ferramenta de terminal.


transform_terminal_output​

Dispara dentro da ferramenta terminal, depois que a captura do processo em primeiro plano já foi limitada pelo ambiente, e antes do limite final de saída. Deixa os plugins substituírem o stdout/stderr capturado; a substituição continua sujeita ao limite final de saída.

Assinatura do callback:

def my_callback(
command: str,
output: str,
returncode: int,
task_id: str,
env_type: str,
**kwargs,
) -> str | None:
ParâmetroTipoDescrição
commandstrO comando de shell que produziu a saída.
outputstrstdout e stderr combinados, depois da captura limitada do processo.
returncodeintCódigo de retorno do processo.
task_idstrIdentificador efetivo da tarefa, ou uma string vazia.
env_typestrTipo do ambiente de execução.

Valor de retorno: a primeira str substitui a saída; None deixa como está. Comando e saída podem conter credenciais ou outros dados sensíveis.

def summarize_find(command, output, **kwargs):
if command.startswith("find ") and len(output) > 50_000:
lines = output.count("\n")
head = "\n".join(output.splitlines()[:40])
return f"{head}\n\n[resumo: {lines} caminhos no total, mostrando os 40 primeiros]"
return None

def register(ctx):
ctx.register_hook("transform_terminal_output", summarize_find)

Faz par com o transform_tool_result, que roda depois, para toda ferramenta, inclusive o terminal.


transform_llm_output​

Dispara uma vez por turno, depois que o laço de chamada de ferramentas termina e o modelo produziu uma resposta final, antes de essa resposta ser entregue ao usuário (CLI, gateway ou chamador programático). Deixa um plugin reescrever o texto final do assistente com métodos de programação clássica — sem queimar tokens de inferência extras em texto de sabor do SOUL nem numa transformação por skill.

Assinatura do callback:

def my_callback(
response_text: str,
session_id: str,
model: str,
platform: str,
**kwargs,
) -> str | None:
ParâmetroTipoDescrição
response_textstrO texto de resposta final do assistente neste turno.
session_idstrID de sessão desta conversa (pode vir vazio em execuções de uso único).
modelstrNome do modelo que produziu a resposta (por exemplo anthropic/claude-sonnet-4.6).
platformstrPlataforma de entrega (cli, telegram, discord, …; vazio quando não definida).

Valor de retorno: uma str não vazia para substituir o texto da resposta; None ou string vazia para deixar como está. Vence a primeira string não vazia quando vários plugins se registram. Diferente das transformações de ferramenta e de terminal, uma string vazia não é aceita como substituição.

Para que serve: aplicar uma transformação de personalidade/vocabulário (falar como pirata, como o Bob Esponja), redigir identificadores específicos do usuário no texto final, acrescentar um rodapé de assinatura do projeto, impor um guia de estilo da casa sem queimar tokens em instruções do SOUL.

Quando o streaming da CLI está ligado, uma transformação que só acrescenta texto é impressa depois do corpo transmitido. Uma transformação que substitui a resposta é impressa por inteiro depois do corpo transmitido, marcada como transformação pós-fluxo, para o conteúdo substituto nunca se perder em silêncio.

import os, re

def spongebob(response_text, **kwargs):
if os.environ.get("SPONGEBOB_MODE") != "on":
return None # passa adiante sem mudar
return re.sub(r"!", "!! Molho tártaro!", response_text)

def register(ctx):
ctx.register_hook("transform_llm_output", spongebob)

O hook é protegido por uma resposta não vazia e não interrompida — ele não dispara em interrupções pelo botão de parar nem em turnos vazios. Exceções são registradas como aviso e não quebram a execução do agente.

Hooks observadores de requisição à API​

pre_api_request​

Dispara a cada tentativa de provedor, imediatamente antes do envio. É só de observação. Os campos legados user_message, conversation_history e request_messages são crus e, de propósito, não saneados, por compatibilidade; consumidores novos devem preferir o envelope request saneado.

post_api_request​

Dispara depois que uma resposta do provedor foi normalizada com sucesso. É só de observação. Prefira o response saneado; o assistant_message é a mensagem normalizada crua, e o usage contém dados contábeis.

api_request_error​

Dispara numa tentativa de provedor que falhou, com status e tempos de nova tentativa, um objeto error e o request saneado. É só de observação. As mensagens de erro ainda podem conter dados do provedor ou do usuário.

on_skill_lifecycle​

Dispara depois de uma mudança autoritativa de estado de uso de skill. É só de observação e expõe o skill_name local, a procedência, os IDs de correlação, a contagem de uso e as marcas de reaproveitamento.

Observadores do ciclo de vida do kanban​

kanban_task_claimed​

Dispara depois do commit da reivindicação, no processo despachante, imediatamente antes de criar o trabalhador.

kanban_task_completed​

Dispara depois da conclusão e da limpeza, em geral no processo trabalhador. O summary dele pode conter conteúdo do projeto ou do usuário.

kanban_task_blocked​

Dispara depois de uma transição normal para bloqueado. O caminho de espera por dependência o invoca antes de aquela transação de escrita sair. O reason dele pode conter conteúdo do projeto ou do usuário.

Os três hooks de kanban são só de observação e carregam task_id, profile_name, board, assignee e run_id; o de conclusão acrescenta summary, e o de bloqueio acrescenta reason.

Observadores de ciclo de vida do trabalhador, mutação de tarefa e despacho do kanban​

Cinco observadores adicionais (RFC #58548) estendem a família kanban. Todos são só de observação, disparam depois que a transação relevante é commitada, e saem cedo em has_hook — sem assinante, o comportamento do despacho não muda. Hooks com escopo de tarefa carregam os mesmos campos comuns dos hooks acima.

  • on_kanban_worker_spawned — depois que o spawn_fn retorna e o PID do trabalhador é persistido. Acrescenta worker_pid (pode ser None) e workspace_path. Roda dentro do lock de despacho; mantenha os callbacks rápidos.
  • on_kanban_worker_exited — derivado do tique, quando o detect_crashed_workers recupera uma tarefa de PID morto. Acrescenta worker_pid, exit_kind, exit_code, outcome, retry_status.
  • on_kanban_worker_stale_claim — quando uma reivindicação com TTL vencido é recuperada; extensões com PID vivo não disparam. Acrescenta worker_pid, heartbeat_stale, retry_status.
  • on_kanban_task_updated — depois de uma escrita commitada em campo de tarefa fora do ciclo de reivindicar/concluir/bloquear (assign_task, sobrescritas de modelo/raciocínio, editores do painel). Acrescenta changed_fields — só nomes de campo, nunca valores.
  • on_kanban_dispatch_tick — uma vez por tique do despachante, estritamente depois de o lock de despacho ser liberado, incluindo tiques ociosos e com disputa de lock. Payload: board, profile_name, dry_run, outcome, result.

Hooks de shell​

Declare hooks em script de shell no seu ~/.hermes/config.yaml e o Hermes vai rodá-los como subprocessos sempre que o evento de hook de plugin correspondente disparar — em sessões tanto da CLI quanto do gateway. Não é preciso escrever plugin em Python.

Use hooks de shell quando você quer um script pronto, de arquivo único (Bash, Python, qualquer coisa com shebang), para:

  • Bloquear ou modificar uma chamada de ferramenta — rejeitar comandos perigosos do terminal, impor políticas por diretório, exigir aprovação para operações destrutivas de write_file / patch, ou reescrever argumentos (sanear caminhos, injetar padrões) antes de a ferramenta rodar.
  • Rodar depois de uma chamada de ferramenta — formatar automaticamente arquivos Python ou TypeScript que o agente acabou de escrever, registrar chamadas de API, disparar um fluxo de CI.
  • Injetar contexto no próximo turno do LLM — colocar a saída do git status, o dia da semana atual ou documentos recuperados antes da mensagem do usuário (veja pre_llm_call).
  • Observar eventos de ciclo de vida — escrever uma linha de log quando um subagente termina (subagent_stop) ou quando uma sessão começa (on_session_start).

Os hooks de shell são registrados pela chamada de agent.shell_hooks.register_from_config(cfg), tanto na partida da CLI (hermes_cli/main.py) quanto na partida do gateway (gateway/run.py). Eles se combinam naturalmente com os hooks de plugin em Python — os dois passam pelo mesmo despachante.

Comparação rápida​

DimensãoHooks de shellHooks de pluginHooks de gateway
Declarados embloco hooks: em ~/.hermes/config.yamlregister() num plugin com plugin.yamldiretório com HOOK.yaml + handler.py
Ficam em~/.hermes/agent-hooks/ (por convenção)~/.hermes/plugins/<name>/~/.hermes/hooks/<name>/
LinguagemQualquer uma (Bash, Python, binário em Go, …)Só PythonSó Python
Rodam emCLI + gatewayCLI + gatewaySó no gateway
EventosVALID_HOOKS (inclusive subagent_stop)VALID_HOOKSCiclo de vida do gateway (gateway:startup, agent:*, command:*)
Podem bloquear uma chamada de ferramentaSim (pre_tool_call)Sim (pre_tool_call)Não
Podem injetar contexto no LLMSim (pre_llm_call)Sim (pre_llm_call)Não
ConsentimentoPedido no primeiro uso, por par (event, command)Implícito (confiança no plugin Python)Implícito (confiança no diretório)
Isolamento entre processosSim (subprocesso)Não (no mesmo processo)Não (no mesmo processo)

Esquema de configuração​

hooks:
<event_name>: # Precisa estar em VALID_HOOKS
- matcher: "<regex>" # Opcional; usado só em pre/post_tool_call
command: "<comando de shell>" # Obrigatório; roda via shlex.split, shell=False
timeout: <segundos> # Opcional; padrão 60, limitado a 300
fail_closed: <bool> # Opcional; padrão false. Só em pre_tool_call.
# `failClosed` também é aceito (compat. Cursor/Claude Code)

hooks_auto_accept: false # Veja "Modelo de consentimento" abaixo

Os nomes de evento precisam ser um dos eventos de hook de plugin; erros de digitação geram um aviso "Did you mean X?" e são pulados. Chaves desconhecidas dentro de uma entrada são ignoradas; a falta de command é pulada com aviso. timeout > 300 é limitado com aviso. fail_closed: true num evento que não seja pre_tool_call gera aviso e é ignorado (só eventos capazes de bloquear podem falhar fechado).

Protocolo de fio em JSON​

A cada disparo do evento, o Hermes cria um subprocesso para cada hook correspondente (respeitando o matcher), manda um payload JSON pelo stdin e lê o stdout de volta como JSON.

stdin — payload que o script recebe:

{
"hook_event_name": "pre_tool_call",
"tool_name": "terminal",
"tool_input": {"command": "rm -rf /"},
"session_id": "sess_abc123",
"cwd": "/home/user/project",
"extra": {"task_id": "...", "tool_call_id": "..."}
}

tool_name e tool_input são null em eventos que não são de ferramenta (pre_llm_call, subagent_stop, ciclo de vida de sessão). O dicionário extra carrega todos os argumentos nomeados específicos do evento (user_message, conversation_history, child_role, duration_ms, …). Valores que não podem ser serializados viram string, em vez de serem omitidos.

stdout — resposta opcional:

// Bloquear um pre_tool_call (os dois formatos são aceitos; normalizados internamente):
{"decision": "block", "reason": "Forbidden: rm -rf"} // estilo Claude Code
{"action": "block", "message": "Forbidden: rm -rf"} // canônico do Hermes

// Modificar um pre_tool_call — reescrever os argumentos da ferramenta antes do despacho:
{"action": "modify", "args": {"new_string": "fixed content"}} // canônico do Hermes
{"decision": "modify", "tool_input": {"new_string": "fixed content"}} // estilo Claude Code

// Injetar contexto no pre_llm_call:
{"context": "Hoje é sexta-feira, 2026-04-17"}

// Manter o agente andando no portão de verificação (pre_verify); os dois formatos são aceitos:
{"action": "continue", "message": "Rode o formatador e então termine."}
{"decision": "block", "reason": "Rode o formatador e então termine."}

// Sem efeito, em silêncio — qualquer saída vazia / que não corresponda serve:

JSON malformado, códigos de saída diferentes de zero e tempos esgotados geram um aviso, mas nunca abortam o laço do agente.

Código de saída 2 = bloqueio (compatível com Claude Code / Cursor)​

Um hook de pre_tool_call que sai com o código 2 bloqueia a chamada de ferramenta mesmo quando o stdout dele não traz JSON de bloqueio. A mensagem de bloqueio é resolvida nesta ordem de prioridade:

  1. o JSON de bloqueio no stdout (reason / message), quando existe;
  2. os primeiros 400 caracteres do stderr;
  3. um padrão genérico, "Blocked by shell hook.".

Então o hook de bloqueio mais simples possível é:

#!/usr/bin/env bash
echo "violação de política: rm -rf não é permitido" >&2
exit 2

Nos eventos em que a diretiva de bloqueio não é honrada (todos menos o pre_tool_call), a saída 2 é tratada como qualquer outra saída diferente de zero: um aviso é registrado e o stdout continua sendo interpretado.

Falhar aberto ou falhar fechado​

Por padrão, os hooks de shell falham abertos: um erro ao criar o processo, um tempo esgotado ou um stdout impossível de interpretar geram um aviso e a ação segue. Esse é o padrão certo para hooks de observabilidade — mas é errado para portões de segurança. Um verificador de segredos que quebrou não pode liberar em silêncio a chamada de ferramenta que ele deveria examinar.

Defina fail_closed: true (ou failClosed: true, a grafia do Cursor/Claude Code) numa entrada de pre_tool_call para inverter isso:

hooks:
pre_tool_call:
- matcher: "terminal|write_file|patch"
command: "~/.hermes/agent-hooks/secret-scan.sh"
timeout: 10
fail_closed: true

Com fail_closed: true, cada uma destas situações agora bloqueia a chamada de ferramenta, com hook <command> failed closed: <reason>:

FalhaFalha aberta (padrão)fail_closed: true
Comando não encontrado / não executávelavisa, seguebloqueia
Tempo esgotadoavisa, seguebloqueia
stdout que não é JSON (por exemplo, um stack trace)avisa, seguebloqueia
Saída limpa, JSON válido sem efeito ({})seguesegue

O fail_closed só vale para eventos capazes de bloquear (hoje, o pre_tool_call); defini-lo em qualquer outro evento gera um aviso na leitura da configuração e é ignorado. O hermes hooks test reflete essas semânticas — a linha parsed mostra exatamente o formato de bloqueio que o despachante receberia.

Exemplos práticos​

1. Formatar arquivos Python automaticamente depois de cada escrita​

# ~/.hermes/config.yaml
hooks:
post_tool_call:
- matcher: "write_file|patch"
command: "~/.hermes/agent-hooks/auto-format.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/auto-format.sh
payload="$(cat -)"
path=$(echo "$payload" | jq -r '.tool_input.path // empty')
[[ "$path" == *.py ]] && command -v black >/dev/null && black "$path" 2>/dev/null
printf '{}\n'

A visão que o agente tem do arquivo no contexto dele não é relida automaticamente — a reformatação só afeta o arquivo em disco. As próximas chamadas de read_file pegam a versão formatada.

2. Bloquear comandos destrutivos do terminal​

hooks:
pre_tool_call:
- matcher: "terminal"
command: "~/.hermes/agent-hooks/block-rm-rf.sh"
timeout: 5
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/block-rm-rf.sh
payload="$(cat -)"
cmd=$(echo "$payload" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-rf?[[:space:]]+/'; then
printf '{"decision": "block", "reason": "bloqueado: rm -rf / não é permitido"}\n'
else
printf '{}\n'
fi

3. Injetar o git status em todo turno (equivalente ao UserPromptSubmit do Claude Code)​

hooks:
pre_llm_call:
- command: "~/.hermes/agent-hooks/inject-cwd-context.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/inject-cwd-context.sh
cat - >/dev/null # descarta o payload do stdin
if status=$(git status --porcelain 2>/dev/null) && [[ -n "$status" ]]; then
jq --null-input --arg s "$status" \
'{context: ("Mudanças não commitadas no cwd:\n" + $s)}'
else
printf '{}\n'
fi

O evento UserPromptSubmit do Claude Code de propósito não é um evento separado no Hermes — o pre_llm_call dispara no mesmo lugar e já aceita injeção de contexto. Use ele aqui.

4. Registrar toda conclusão de subagente​

hooks:
subagent_stop:
- command: "~/.hermes/agent-hooks/log-orchestration.sh"
#!/usr/bin/env bash
# ~/.hermes/agent-hooks/log-orchestration.sh
log=~/.hermes/logs/orchestration.log
jq -c '{ts: now, parent: .session_id, extra: .extra}' < /dev/stdin >> "$log"
printf '{}\n'

Cada par (event, command) único pede aprovação ao usuário na primeira vez que o Hermes o vê, e depois persiste a decisão em ~/.hermes/shell-hooks-allowlist.json. As execuções seguintes (CLI ou gateway) pulam o pedido.

Três saídas de emergência contornam o pedido interativo — qualquer uma basta:

  1. a opção --accept-hooks na CLI (por exemplo, hermes --accept-hooks chat)
  2. a variável de ambiente HERMES_ACCEPT_HOOKS=1
  3. hooks_auto_accept: true no ~/.hermes/config.yaml

Execuções sem TTY (gateway, cron, CI) precisam de uma dessas três — senão, qualquer hook recém-adicionado fica sem registro em silêncio e gera um aviso.

Edições no script são confiadas em silêncio. A lista de permissões é indexada pela string exata do comando, não pelo hash do script, então editar o script em disco não invalida o consentimento. O hermes hooks doctor sinaliza mudança de mtime, para você notar as edições e decidir se aprova de novo.

Colocar na lista de permissões manualmente​

Colocar na lista manualmente é útil em implantações sem TTY ou com conta de serviço, em que um operador não consegue responder ao pedido de primeiro uso de forma interativa. O arquivo da lista de permissões é o ~/.hermes/shell-hooks-allowlist.json, e o formato esperado é um array approvals. Cada aprovação registra o event do hook e a string exata do command:

{
"approvals": [
{
"event": "post_llm_call",
"command": "/home/hermes/.hermes/hooks/my-hook.py"
}
]
}

A string do comando precisa bater exatamente com o comando configurado no hook. Um objeto indexado por caminho com um campo sha256 não é o formato esperado e não vai aprovar o hook. Confira as entradas manuais com hermes hooks list.

A CLI hermes hooks​

ComandoO que faz
hermes hooks listDespeja os hooks configurados com matcher, timeout e situação do consentimento
hermes hooks test <event> [--for-tool X] [--payload-file F]Dispara todo hook correspondente contra um payload sintético e imprime a resposta interpretada
hermes hooks revoke <command>Remove toda entrada da lista de permissões que bata com <command> (vale a partir do próximo reinício)
hermes hooks doctorPara cada hook configurado: verifica o bit de execução, a situação na lista de permissões, a mudança de mtime, a validade da saída JSON e o tempo aproximado de execução

Segurança​

Os hooks de shell rodam com as suas credenciais completas de usuário — o mesmo limite de confiança de uma entrada de cron ou de um alias de shell. Trate o bloco hooks: do config.yaml como configuração privilegiada:

  • Só referencie scripts que você escreveu ou revisou por inteiro.
  • Mantenha os scripts dentro de ~/.hermes/agent-hooks/, para o caminho ser fácil de auditar.
  • Rode o hermes hooks doctor de novo depois de puxar uma configuração compartilhada, para notar hooks recém-adicionados antes de eles se registrarem.
  • Se o seu config.yaml fica em controle de versão compartilhado com um time, revise os PRs que mexem na seção hooks: com o mesmo cuidado com que você revisaria configuração de CI.

Ordem e precedência​

Tanto os hooks de plugin em Python quanto os hooks de shell passam pelo mesmo despachante invoke_hook(). Os plugins Python são registrados primeiro (discover_and_load()) e os hooks de shell depois (register_from_config()), então as decisões de bloqueio de pre_tool_call em Python têm precedência em caso de empate. Vence o primeiro bloqueio válido — o agregador retorna assim que qualquer callback produz {"action": "block", "message": str} com uma mensagem não vazia.

Webhooks de saída​

Os webhooks de saída são o espelho, do lado de empurrar, da plataforma de webhooks de entrada: webhooks de entrada acordam o Hermes quando o mundo muda; webhooks de saída contam ao mundo quando o Hermes faz algo. Configure uma lista de endpoints HTTP e os eventos de ciclo de vida que interessam a cada um, e o Hermes faz um POST de um payload JSON assinado para cada endpoint sempre que um evento correspondente dispara — sem ninguém do lado de lá ficar consultando.

Usos típicos:

  • Avisar um sistema de CI ou um painel quando um turno do agente termina (on_session_end)
  • Acompanhar conclusões de subagente numa frota inteira (subagent_stop)
  • Alimentar monitoramento externo com atividade de ferramentas (post_tool_call com um matcher)
  • Acordar outra instância do Hermes: aponte a URL para o webhook de entrada daquela instância

Configuração​

Acrescente uma lista hooks.outbound: ao ~/.hermes/config.yaml:

hooks:
outbound:
- name: ci-notify # rótulo opcional para os logs
url: https://ci.example.com/hermes-events
events: [on_session_end, subagent_stop]
secret_env: HERMES_OUTBOUND_WEBHOOK_SECRET # variável de ambiente com o segredo HMAC
timeout: 10 # segundos por tentativa (1–60)

- name: tool-monitor
url: https://metrics.example.com/hooks/hermes
events: [post_tool_call]
matcher: "terminal|delegate_task" # regex, só em eventos com escopo de ferramenta

Qualquer evento do conjunto de hooks de plugin é válido (pre_tool_call, post_tool_call, pre_llm_call, post_llm_call, on_session_start, on_session_end, subagent_start, subagent_stop, ...). Entradas malformadas geram aviso e são puladas — um webhook quebrado nunca derruba o agente. As mudanças passam a valer na próxima sessão da CLI ou no próximo reinício do gateway.

Segredos: prefira secret_env (o nome de uma variável de ambiente, em geral definida em ~/.hermes/.env) a um literal secret: embutido, para o arquivo de configuração ficar livre de credenciais. Entradas sem segredo são entregues sem assinatura (marcadas como UNSIGNED pelo hermes hooks list).

Formato de fio​

Cada disparo faz um POST com um corpo JSON de mesmo formato de primeiro nível do stdin dos hooks de shell, mais metadados de entrega:

{
"hook_event_name": "on_session_end",
"tool_name": null,
"tool_input": null,
"session_id": "sess_abc123",
"cwd": "/home/user/project",
"extra": {"completed": true, "interrupted": false, "model": "...", "platform": "cli"},
"delivery_id": "3f2c9a...",
"timestamp": "2026-07-22T14:00:00Z"
}

Cabeçalhos:

CabeçalhoValor
Content-Typeapplication/json
X-Hermes-EventO nome do evento de hook
X-Hermes-DeliveryId único por entrega — o mesmo valor de delivery_id no corpo
X-Hermes-Signature-256sha256=<hex> — HMAC-SHA256 do corpo cru, no estilo do GitHub; só aparece quando há um segredo configurado

Verifique a assinatura exatamente como você faria com um webhook do GitHub:

import hashlib, hmac

def verify(body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)

Como delivery_id e timestamp ficam dentro do corpo assinado, um receptor que verifica a assinatura também ganha proteção contra repetição de graça:

  • Deduplique pelo delivery_id (ou pelo cabeçalho X-Hermes-Delivery correspondente) — guarde os ids vistos recentemente e pule os repetidos. O Hermes tenta reenviar entregas falhas uma vez, então o mesmo id pode chegar duas vezes de forma legítima.
  • Rejeite eventos velhos comparando o timestamp com o seu relógio, com uma janela de tolerância (5 minutos é o padrão comum). Um atacante que repita uma requisição capturada não consegue forjar um timestamp novo sem o segredo.

Semântica de entrega​

  • Dispara e esquece, fora do caminho quente. Os eventos são serializados e enfileirados na hora; uma única thread em segundo plano faz os POSTs HTTP. Um endpoint lento ou morto nunca consegue travar uma chamada de ferramenta ou um turno do agente.
  • Só notificação. Diferente dos hooks de shell, os webhooks de saída não conseguem bloquear chamadas de ferramenta nem injetar contexto — o corpo da resposta é ignorado. Eles observam, nunca conduzem.
  • Novas tentativas limitadas. Erros de conexão e respostas 5xx são tentados de novo uma vez, com espera progressiva; respostas 4xx não são repetidas (o receptor disse que a requisição em si está errada). As falhas são registradas e descartadas — a entrega é de melhor esforço, não garantida.
  • Redirecionamentos nunca são seguidos. Uma resposta 3xx é tratada como erro de configuração e registrada — seguir um POST redirecionado descartaria o payload assinado em silêncio. Aponte a url para o endpoint final.
  • Fila limitada. Se a fila acumula (endpoint morto, enxurrada de eventos), os eventos novos são descartados com um aviso, em vez de consumir memória sem limite.
  • Sem pedido de consentimento. Os destinos de saída não executam código na sua máquina — eles recebem dados numa URL que você configurou. O HERMES_SAFE_MODE=1 continua pulando o registro, igual acontece com plugins e hooks de shell. Note que os payloads incluem entradas de ferramenta e metadados de evento, então só aponte para endpoints em que você confia, e prefira https://.

O hermes hooks list mostra os destinos de saída configurados ao lado dos hooks de shell, incluindo se cada destino está assinado.