Webhooks

Receive events from external services (GitHub, GitLab, JIRA, Stripe, etc.) and trigger Hermes agent runs automatically. The webhook adapter runs an HTTP server that accepts POST requests, validates HMAC signatures, transforms payloads into agent prompts, and routes responses back to the source or to another configured platform.

The agent processes the event and can respond by posting comments on PRs, sending messages to Telegram/Discord, or logging the result.


Quick Start

  1. Enable via hermes gateway setup or environment variables
  2. Define routes in config.yaml or create them dynamically with hermes webhook subscribe
  3. Point your service at http://your-server:8644/webhooks/<route-name>

Setup

There are two ways to enable the webhook adapter.

Via setup wizard

hermes gateway setup

Follow the prompts to enable webhooks, set the port, and set a global HMAC secret.

Via environment variables

Add to ~/.hermes/.env:

WEBHOOK_ENABLED=true
WEBHOOK_PORT=8644        # default
WEBHOOK_SECRET=your-global-secret

Verify the server

Once the gateway is running:

curl http://localhost:8644/health

Expected response:

{"status": "ok", "platform": "webhook"}

Configuring Routes

Routes define how different webhook sources are handled. Each route is a named entry under platforms.webhook.extra.routes in your config.yaml.

Route properties

PropertyRequiredDescription
eventsNoList of event types to accept (e.g. ["pull_request"]). If empty, all events are accepted. Event type is read from X-GitHub-Event, X-GitLab-Event, or event_type in the payload.
secretYesHMAC secret for signature validation. Falls back to the global secret if not set on the route. Set to "INSECURE_NO_AUTH" for testing only (skips validation).
promptNoTemplate string with dot-notation payload access (e.g. {pull_request.title}). If omitted, the full JSON payload is dumped into the prompt.
skillsNoList of skill names to load for the agent run.
deliverNoWhere to send the response: github_comment, telegram, discord, slack, signal, sms, whatsapp, matrix, mattermost, homeassistant, email, dingtalk, feishu, wecom, weixin, bluebubbles, qqbot, or log (default).
deliver_extraNoAdditional delivery config — keys depend on deliver type (e.g. repo, pr_number, chat_id). Values support the same {dot.notation} templates as prompt.

Full example

platforms:
  webhook:
    enabled: true
    extra:
      port: 8644
      secret: "global-fallback-secret"
      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}
            Diff URL: {pull_request.diff_url}
            Action: {action}            
          skills: ["github-code-review"]
          deliver: "github_comment"
          deliver_extra:
            repo: "{repository.full_name}"
            pr_number: "{number}"
        deploy-notify:
          events: ["push"]
          secret: "deploy-secret"
          prompt: "New push to {repository.full_name} branch {ref}: {head_commit.message}"
          deliver: "telegram"

Prompt Templates

Prompts use dot-notation to access nested fields in the webhook payload:

You can mix {__raw__} with regular template variables:

prompt: "PR #{pull_request.number} by {pull_request.user.login}: {__raw__}"

If no prompt template is configured for a route, the entire payload is dumped as indented JSON (truncated at 4000 characters).

The same dot-notation templates work in deliver_extra values.

Forum Topic Delivery

When delivering webhook responses to Telegram, you can target a specific forum topic by including message_thread_id (or thread_id) in deliver_extra:

webhooks:
  routes:
    alerts:
      events: ["alert"]
      prompt: "Alert: {__raw__}"
      deliver: "telegram"
      deliver_extra:
        chat_id: "-1001234567890"
        message_thread_id: "42"

If chat_id is not provided in deliver_extra, the delivery falls back to the home channel configured for the target platform.


GitHub PR Review (Step by Step)

This walkthrough sets up automatic code review on every pull request.

1. Create the webhook in GitHub

  1. Go to your repository → SettingsWebhooksAdd webhook
  2. Set Payload URL to http://your-server:8644/webhooks/github-pr
  3. Set Content type to application/json
  4. Set Secret to match your route config (e.g. github-webhook-secret)
  5. Under Which events?, select Let me select individual events and check Pull requests
  6. Click Add webhook

2. Add the route config

Add the github-pr route to your ~/.hermes/config.yaml as shown in the example above.

3. Ensure gh CLI is authenticated

The github_comment delivery type uses the GitHub CLI to post comments:

gh auth login

4. Test it

Open a pull request on the repository. The webhook fires, Hermes processes the event, and posts a review comment on the PR.


GitLab Webhook Setup

GitLab webhooks work similarly but use a different authentication mechanism. GitLab sends the secret as a plain X-Gitlab-Token header (exact string match, not HMAC).

1. Create the webhook in GitLab

  1. Go to your project → SettingsWebhooks
  2. Set the URL to http://your-server:8644/webhooks/gitlab-mr
  3. Enter your Secret token
  4. Select Merge request events (and any other events you want)
  5. Click Add webhook

2. Add the route config

platforms:
  webhook:
    enabled: true
    extra:
      routes:
        gitlab-mr:
          events: ["merge_request"]
          secret: "your-gitlab-secret-token"
          prompt: |
            Review this merge request:
            Project: {project.path_with_namespace}
            MR !{object_attributes.iid}: {object_attributes.title}
            Author: {object_attributes.last_commit.author.name}
            URL: {object_attributes.url}
            Action: {object_attributes.action}            
          deliver: "log"

Delivery Options

The deliver field controls where the agent’s response goes after processing the webhook event.

Deliver TypeDescription
logLogs the response to the gateway log output. This is the default and is useful for testing.
github_commentPosts the response as a PR/issue comment via the gh CLI. Requires deliver_extra.repo and deliver_extra.pr_number. The gh CLI must be installed and authenticated on the gateway host (gh auth login).
telegramRoutes the response to Telegram. Uses the home channel, or specify chat_id in deliver_extra.
discordRoutes the response to Discord. Uses the home channel, or specify chat_id in deliver_extra.
slackRoutes the response to Slack. Uses the home channel, or specify chat_id in deliver_extra.
signalRoutes the response to Signal. Uses the home channel, or specify chat_id in deliver_extra.
smsRoutes the response to SMS via Twilio. Uses the home channel, or specify chat_id in deliver_extra.
whatsappRoutes the response to WhatsApp. Uses the home channel, or specify chat_id in deliver_extra.
matrixRoutes the response to Matrix. Uses the home channel, or specify chat_id in deliver_extra.
mattermostRoutes the response to Mattermost. Uses the home channel, or specify chat_id in deliver_extra.
homeassistantRoutes the response to Home Assistant. Uses the home channel, or specify chat_id in deliver_extra.
emailRoutes the response to Email. Uses the home channel, or specify chat_id in deliver_extra.
dingtalkRoutes the response to DingTalk. Uses the home channel, or specify chat_id in deliver_extra.
feishuRoutes the response to Feishu/Lark. Uses the home channel, or specify chat_id in deliver_extra.
wecomRoutes the response to WeCom. Uses the home channel, or specify chat_id in deliver_extra.
weixinRoutes the response to Weixin (WeChat). Uses the home channel, or specify chat_id in deliver_extra.
bluebubblesRoutes the response to BlueBubbles (iMessage). Uses the home channel, or specify chat_id in deliver_extra.

For cross-platform delivery, the target platform must also be enabled and connected in the gateway. If no chat_id is provided in deliver_extra, the response is sent to that platform’s configured home channel.


Dynamic Subscriptions (CLI)

In addition to static routes in config.yaml, you can create webhook subscriptions dynamically using the hermes webhook CLI command. This is especially useful when the agent itself needs to set up event-driven triggers.

Create a subscription

hermes webhook subscribe github-issues \
  --events "issues" \
  --prompt "New issue #{issue.number}: {issue.title}\nBy: {issue.user.login}\n\n{issue.body}" \
  --deliver telegram \
  --deliver-chat-id "-100123456789" \
  --description "Triage new GitHub issues"

This returns the webhook URL and an auto-generated HMAC secret. Configure your service to POST to that URL.

List subscriptions

hermes webhook list

Remove a subscription

hermes webhook remove github-issues

Test a subscription

hermes webhook test github-issues
hermes webhook test github-issues --payload '{"issue": {"number": 42, "title": "Test"}}'

How dynamic subscriptions work

Agent-driven subscriptions

The agent can create subscriptions via the terminal tool when guided by the webhook-subscriptions skill. Ask the agent to “set up a webhook for GitHub issues” and it will run the appropriate hermes webhook subscribe command.


Security

The webhook adapter includes multiple layers of security:

HMAC signature validation

The adapter validates incoming webhook signatures using the appropriate method for each source:

If a secret is configured but no recognized signature header is present, the request is rejected.

Secret is required

Every route must have a secret — either set directly on the route or inherited from the global secret. Routes without a secret cause the adapter to fail at startup with an error. For development/testing only, you can set the secret to "INSECURE_NO_AUTH" to skip validation entirely.

Rate limiting

Each route is rate-limited to 30 requests per minute by default (fixed-window). Configure this globally:

platforms:
  webhook:
    extra:
      rate_limit: 60  # requests per minute

Requests exceeding the limit receive a 429 Too Many Requests response.

Idempotency

Delivery IDs (from X-GitHub-Delivery, X-Request-ID, or a timestamp fallback) are cached for 1 hour. Duplicate deliveries (e.g. webhook retries) are silently skipped with a 200 response, preventing duplicate agent runs.

Body size limits

Payloads exceeding 1 MB are rejected before the body is read. Configure this:

platforms:
  webhook:
    extra:
      max_body_bytes: 2097152  # 2 MB

Prompt injection risk

Warning Webhook payloads contain attacker-controlled data — PR titles, commit messages, issue descriptions, etc. can all contain malicious instructions. Run the gateway in a sandboxed environment (Docker, VM) when exposed to the internet. Consider using the Docker or SSH terminal backend for isolation.


Troubleshooting

Webhook not arriving

Signature validation failing

Event being ignored

Agent not responding

Duplicate responses

gh CLI errors (GitHub comment delivery)


Environment Variables

VariableDescriptionDefault
WEBHOOK_ENABLEDEnable the webhook platform adapterfalse
WEBHOOK_PORTHTTP server port for receiving webhooks8644
WEBHOOK_SECRETGlobal HMAC secret (used as fallback when routes don’t specify their own)(none)