Cómo configurar un webhook en Hermes Agent paso a paso
Un compañero me enseñó, orgulloso, su automatización de code review: un cron cada cinco minutos que llamaba a la API de GitHub y, si aparecía un PR nuevo, lanzaba el agente.
Funcionaba. Más o menos.
Cuando GitHub tardaba, se duplicaban las revisiones. Cuando el proceso moría de madrugada, nadie se enteraba hasta el lunes.
El problema no era el agente. Era que estaba preguntando en lugar de escuchar.
Un webhook en Hermes Agent invierte esa relación: en vez de sondear, dejas que GitHub, GitLab o cualquier servicio que hable HTTP llamen a tu puerta con la firma criptográfica verificada, y el agente reacciona solo cuando de verdad ha pasado algo.
Una aclaración antes de seguir, porque me lo preguntáis mucho: Hermes Agent es el agente autónomo open source de Nous Research, no un producto mío. Yo hago contenido sobre él porque me parece una de las piezas más interesantes del ecosistema agéntico actual.
Todo lo que sigue está verificado contra la documentación oficial de Hermes Agent en julio de 2026. El adaptador webhook sigue evolucionando, así que contrasta con la doc si tu instalación es posterior.
Los 7 pasos para configurar un webhook en Hermes Agent
- Activa el adaptador con
hermes gateway setupo las variablesWEBHOOK_*en~/.hermes/.env. - Comprueba que el servidor responde en
http://localhost:8644/health. - Define la ruta dentro de
platforms.webhook.extra.routesen~/.hermes/config.yaml. - Elige el esquema de firma del proveedor y valida el HMAC (usa el genérico V2).
- Dispara la ruta a mano con
hermes webhook testantes de conectar el proveedor real. - Marca con
deliver_only: truelas rutas que no necesitan que el agente razone. - Ajusta y lista las rutas desde la CLI sin volver a editar YAML.
Tiempo estimado: 15 minutos.
Necesitas: Hermes Agent instalado, acceso a la configuración de webhooks del proveedor y una URL pública (o un túnel) que llegue a tu puerto.
Vamos al lío.
Qué es un webhook en Hermes Agent y qué hace el adaptador
Un webhook en Hermes Agent es una ruta HTTP que recibe eventos POST de un servicio externo, valida su firma HMAC y los convierte en una ejecución del agente. Lo gestiona el adaptador webhook, que levanta un servidor HTTP y por cada petición hace cuatro cosas en orden:
- Valida la firma HMAC del emisor.
- Transforma el payload JSON en un prompt para el agente.
- Ejecuta el agente con ese prompt.
- Enruta la respuesta de vuelta al origen o a otra plataforma que hayas configurado.
Ese paso 4 es el que la gente subestima. No es solo "recibir eventos": es cerrar el círculo. El PR entra por GitHub y el comentario sale por GitHub. O por Telegram. Tú decides.
Paso 1: activa el webhook en Hermes Agent
Puedes activar el adaptador de dos formas: con el asistente interactivo o declarando las variables de entorno. El asistente:
hermes gateway setup
O directamente las variables de entorno en ~/.hermes/.env:
WEBHOOK_ENABLED=true
WEBHOOK_PORT=8644
WEBHOOK_SECRET=your-global-secret
| Variable | Default | Para qué sirve |
|---|---|---|
WEBHOOK_ENABLED |
false |
Activa el adaptador |
WEBHOOK_PORT |
8644 |
Puerto del servidor HTTP |
WEBHOOK_SECRET |
(ninguno) | HMAC global de fallback |
Respeta la separación: la configuración general vive en ~/.hermes/config.yaml y los secretos en ~/.hermes/.env. El día que compartas tu config con alguien lo vas a agradecer.
Paso 2: comprueba que el servidor webhook responde
El adaptador expone un endpoint /health en el puerto configurado. Si devuelve respuesta, está escuchando y puedes seguir:
curl http://localhost:8644/health
Si esto no responde, no sigas. Todo lo demás depende de que el servidor esté escuchando.
Paso 3: define tu primera ruta de webhook en config.yaml
Aquí está el núcleo de todo. Una ruta es un bloque dentro de platforms.webhook.extra.routes en tu config.yaml:
platforms:
webhook:
enabled: true
extra:
port: 8644
secret: "global-fallback-secret"
rate_limit: 30
max_body_bytes: 1048576
routes:
github-pr:
events: ["pull_request"]
secret: "github-webhook-secret"
prompt: |
Review this pull request:
Repository: {repository.full_name}
PR #{number}: {pull_request.title}
Author: {pull_request.user.login}
URL: {pull_request.html_url}
skills: ["github-code-review"]
deliver: "github_comment"
deliver_extra:
repo: "{repository.full_name}"
pr_number: "{number}"
Léelo de arriba abajo y tienes la historia completa: escucha eventos pull_request, valida con este secreto, construye este prompt, usa esta skill y devuelve la respuesta como comentario en el PR.
Estas son las propiedades que puede llevar una ruta:
| Propiedad | Obligatoria | Para qué sirve |
|---|---|---|
events |
No | Tipos de evento a aceptar; si lo dejas vacío, acepta todos |
secret |
Sí* | Secreto HMAC de validación |
prompt |
No | Plantilla con dot-notation; si se omite, vuelca el JSON completo |
skills |
No | Skills que se cargan para esa ejecución del agente |
filters |
No | Filtrado declarativo del payload |
script |
No | Ruta a un script de filtro o transformación propio |
deliver |
No | Destino: github_comment, telegram, discord, slack, log… |
deliver_extra |
No | Configuración del destino (chat_id, repo, pr_number…) |
deliver_only |
No | Salta el agente y envía el prompt como mensaje literal |
*secret es obligatorio salvo que la ruta herede el secreto global.
Sobre las plantillas de prompt, cuatro detalles que te van a morder si no los sabes:
- La dot-notation resuelve rutas anidadas:
{pull_request.title}equivale apayload["pull_request"]["title"]. - Si una clave no existe, se renderiza literalmente como
{clave}. No falla, no avisa. Tu prompt simplemente llega con basura dentro. Este es el error número uno. {__raw__}vuelca el payload entero como JSON indentado, truncado a 4000 caracteres. Muy útil mientras exploras un proveedor nuevo, mala idea en producción.- Las estructuras anidadas se serializan a JSON y se truncan a 2000 caracteres. Si un prompt te llega cortado por la mitad, es esto.
Paso 4: valida la firma HMAC del proveedor
Hermes trae cuatro verificadores de firma. Dos específicos y dos genéricos para todo lo demás:
- GitHub: cabecera
X-Hub-Signature-256, formatosha256=<hex>, HMAC-SHA256 del body. - GitLab: cabecera
X-Gitlab-Token, comparación literal del secreto. - Genérico V2 (el recomendado): cabeceras
X-Webhook-Signature-V2yX-Webhook-Timestamp. El HMAC-SHA256 se calcula sobre<timestamp>.<body>y el timestamp debe caer dentro de ±300 segundos. - Genérico V1 (legacy, deprecado): cabecera
X-Webhook-Signature, HMAC solo del body y sin protección anti-replay.
Usa V2. La diferencia no es cosmética: al meter el timestamp dentro del material firmado, una petición capturada deja de servir pasados cinco minutos. Con V1, un payload robado es válido para siempre.
Y ahora el matiz que separa a quien ha metido esto en producción de quien no: validar el HMAC prueba la identidad del emisor, no que el contenido sea de fiar. Que GitHub firme el evento confirma que viene de GitHub, no que el título del PR no contenga una inyección de prompt escrita por un colaborador externo. Todo campo que venga de fuera se trata como no confiable, siempre. Es la misma disciplina de límites de confianza que aplico al conectar herramientas externas vía servidores MCP.
Paso 5: prueba la ruta con hermes webhook test
No configures una ruta y te quedes mirando GitHub a ver si pica. Hermes trae un comando para dispararla a mano:
hermes webhook test github-issues
hermes webhook test github-issues --payload 39;{"issue": {"number": 42}}39;
Con --payload controlas exactamente qué recibe la plantilla, así que puedes verificar que tu dot-notation resuelve bien antes de que el evento real llegue.
Si el ciclo de "defino el comportamiento, lo pruebo, lo ajusto" te suena a especificar antes de implementar, es exactamente eso. Es el mismo enfoque que desarrollo en el libro de Spec-Driven Development: decide el contrato primero, verifica después.
Paso 6: usa deliver_only para rutas sin coste de LLM
Esta es mi parte favorita y la más ignorada. No todo evento necesita un LLM detrás.
routes:
antenna-matches:
secret: "antenna-webhook-secret"
deliver: "telegram"
deliver_only: true
prompt: "🎉 New match: {match.user_name} matched with you!"
deliver_extra:
chat_id: "{match.telegram_chat_id}"
Con deliver_only: true el prompt renderizado se envía tal cual como mensaje y el agente nunca se invoca. Coste de inferencia: cero.
Un despliegue terminado, un pago recibido, un test que falla: no necesitas que un modelo razone sobre eso, necesitas que llegue a tu Telegram. Reserva el agente para lo que exige criterio y usa deliver_only para el resto. Es la decisión que más reduce la factura de tu stack de IA agéntica.
Paso 7: gestiona las rutas desde la CLI de Hermes
La CLI crea, lista y elimina rutas sin tocar el YAML, que es lo cómodo para iterar:
hermes webhook subscribe github-issues \
--events "issues" \
--prompt "New issue #{issue.number}: {issue.title}\nBy: {issue.user.login}" \
--deliver telegram \
--deliver-chat-id "-100123456789" \
--description "Triage new GitHub issues"
hermes webhook list
hermes webhook remove github-issues
Códigos de respuesta del webhook y qué significa cada uno
El adaptador te dice con precisión qué ha pasado. Aprende esta tabla y te ahorras horas:
| Código | Significado |
|---|---|
200 |
Entregado, o duplicado descartado por idempotencia |
401 |
Firma inválida o ausente |
400 |
JSON malformado |
404 |
Ruta desconocida |
413 |
El body supera max_body_bytes |
429 |
Rate limit superado |
502 |
El destino rechazó la entrega |
Dos protecciones que vienen puestas de serie y conviene conocer: el rate limit por defecto es de 30 peticiones por minuto y por ruta (ajustable con rate_limit), y hay una caché de idempotencia de una hora basada en las cabeceras de delivery ID. Ese reenvío duplicado que rompía el cron de mi compañero aquí devuelve 200 y no ejecuta nada.
Una última nota de seguridad: toda ruta necesita un secreto, propio o heredado del global. INSECURE_NO_AUTH existe, pero solo funciona en loopback (127.0.0.1, localhost, ::1). Está bien pensado: no puedes dejarte una puerta abierta en producción por accidente.
Empieza por lo pequeño
Si vas a hacer una sola cosa hoy, que sea esta: activa el adaptador, crea una ruta con deliver_only: true que te avise por Telegram de algo que ahora mismo miras a mano, y déjala corriendo una semana.
No montes el code review automático el primer día. Comprueba antes que los eventos llegan, que la firma valida y que tus plantillas resuelven. Cuando eso sea aburrido y predecible, le pones el agente detrás.
La documentación de referencia está en la guía oficial de webhooks de Hermes Agent y el código en el repositorio de NousResearch.
Y si lo que quieres es el marco completo —cómo pasar de una idea a un producto real apoyándote en agentes sin acabar con un montón de automatizaciones frágiles— eso es justo lo que enseño en el curso Construye con IA, y lo que practicamos cada semana dentro de Dominicode Labs.
Preguntas frecuentes
¿Necesito exponer mi máquina a internet para usar un webhook en Hermes Agent?
Sí, el proveedor externo tiene que poder alcanzar el puerto donde escucha el adaptador (8644 por defecto), así que necesitas una URL pública o un túnel hacia tu equipo. Para probar en local sin montar nada de eso puedes fijar el secreto de la ruta a INSECURE_NO_AUTH y saltarte la validación de firma, pero Hermes solo lo acepta cuando el gateway escucha en loopback (127.0.0.1, localhost, ::1), precisamente para que no puedas dejarte esa puerta abierta de cara a internet.
¿Qué diferencia hay entre la firma genérica V1 y la V2?
La V2 incluye protección anti-replay y la V1 no. V2 usa las cabeceras X-Webhook-Signature-V2 y X-Webhook-Timestamp, calcula el HMAC-SHA256 sobre <timestamp>.<body> y rechaza cualquier petición cuyo timestamp se salga de ±300 segundos. V1 firma solo el body, está deprecada y una petición capturada sigue siendo válida indefinidamente.
¿Puedo recibir webhooks sin gastar tokens de LLM?
Sí. Añade deliver_only: true a la ruta y Hermes renderiza la plantilla del prompt y la envía como mensaje literal al destino configurado, sin invocar nunca al agente. El coste de inferencia es cero. Es la opción correcta para notificaciones de despliegues, pagos o alertas donde no hace falta ningún razonamiento.
¿Qué pasa si el proveedor reenvía el mismo evento dos veces?
Se descarta. Hermes mantiene una caché de idempotencia de una hora basada en las cabeceras de delivery ID del proveedor, y el duplicado recibe un 200 sin ejecutar el agente de nuevo. Es la protección que hace innecesario el típico registro manual de eventos ya procesados que se monta con sondeo por cron.
¿Es obligatorio poner un secreto en cada ruta?
Sí. Toda ruta necesita un secreto para validar la firma HMAC, aunque puede heredar el valor global definido en WEBHOOK_SECRET o en platforms.webhook.extra.secret en lugar de declarar el suyo propio. Sin secreto válido, las peticiones se rechazan con 401. Lo recomendable es un secreto distinto por ruta.
Mi webhook devuelve 401, ¿qué reviso?
Un 401 significa firma inválida o ausente, casi siempre por desajuste entre el secreto configurado en Hermes y el que registraste en el proveedor. Verifica que coinciden exactamente, que el proveedor envía la cabecera esperada (X-Hub-Signature-256 en GitHub, X-Gitlab-Token en GitLab) y, si usas V2, que el reloj del emisor no se desvía más de 300 segundos.
¿Webhook o polling con cron para disparar un agente?
Webhook, salvo que el proveedor no los ofrezca. El polling introduce latencia igual al intervalo del cron, duplica ejecuciones cuando la API tarda en responder y falla en silencio si el proceso muere. El adaptador webhook reacciona en el momento del evento, descarta reenvíos con su caché de idempotencia de una hora y devuelve un código HTTP que dice exactamente qué ha fallado. El cron solo gana cuando el sistema origen no emite eventos.
¿Por qué mi prompt llega con {algo} sin sustituir?
Porque esa clave no existe en el payload. Hermes renderiza literalmente como {clave} cualquier ruta que no resuelva, sin lanzar error. Dispara la ruta con hermes webhook test <nombre> --payload '<json>' para inspeccionar la estructura real, o usa {__raw__} temporalmente para volcar el payload completo y localizar el nombre correcto del campo.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
