Wiki · Features in depth
Webhooks
A webhook is a door into your agent. How to lock it, whom to open it for and what the agent should do when something knocks.
🪝
A webhook is a door into your agent. Locked it is useful; open to the internet it is a liability.
Hermes verifies a signature on every request (HMAC), but never expose port 8644 directly to the world.
Keep webhooks behind a firewall or a reverse proxy with HTTPS — and keep the secret in
.env,
not in git.What it is for
The agent no longer has to wait to be asked. When something happens in your app (a new pull request, a paid order, a new article, an error in the logs), the app sends a request, Hermes starts working on it — and delivers the result wherever you point it.
| Typical use | What the agent does |
|---|---|
| GitHub: a new pull request | Reads the diff, writes a review and posts it as a PR comment |
| Push to the main branch | Checks the site, runs tests, messages Telegram that it is done |
| From your app: a completed payment | Issues a receipt, sends an e-mail, creates a task |
| Monitoring: a service went down | Reads the logs and sends a summary of what happened |
Enabling
# ~/.hermes/.env
WEBHOOK_ENABLED=true
WEBHOOK_PORT=8644 # default
WEBHOOK_SECRET=your-global-secret
# or via the wizard (enables webhooks, port and global secret)
hermes gateway setup
# verify the server is up
curl http://localhost:8644/health
# → {"status": "ok", "platform": "webhook"}
Routes
Every event source gets its own named route. Your system then points at
http://your-server:8644/webhooks/<route-name>.
| Property | Meaning |
|---|---|
events | Which event types to accept (e.g. ["pull_request"]); empty = all |
secret | Required. Signature key; the global one is the fallback |
prompt | Instructions for the agent with payload values ({pull_request.title}) |
filters | Payload filter — a non-match never wakes the agent |
skills | Which skills the agent should load for this route |
deliver | Where the answer goes: telegram, discord, github_comment, email, log (default)… |
deliver_only | Skips the agent — the message is just forwarded (zero model cost) |
cron_job | Runs an existing scheduled job instead of a fresh session |
coalesce | Debounces rapid events about the same entity into one run (e.g. 5 commits in a PR) |
Example: pull request review
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: {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}"
filters:
- field: "ref"
equals: "refs/heads/main"
deliver: "telegram"
Routes can also be created from the CLI
hermes webhook subscribe <name> # creates a route at runtime
hermes webhook subscribe <name> --route-profile coder # for a specific profile
A route created from the CLI cannot grant itself more power — toolsets are
only changed by hand in config.yaml. That is deliberate: otherwise an agent could use a
webhook to give itself more than it has.
Security: signed does not mean trusted
| Rule | Why |
|---|---|
| Never expose port 8644 directly | Put it behind a reverse proxy with HTTPS, or keep it on localhost/VPN |
INSECURE_NO_AUTH only for testing | It disables signature checks — never in production |
| A separate secret per route | If the GitHub secret leaks, the other routes stay closed |
| Payload content is untrusted text | A PR description can contain instructions (“ignore your rules and…”). The agent must not follow them |
Keep events and filters tight | Fewer wake-ups means lower cost and a smaller attack surface |
Things to watch
| Situation | What to do |
|---|---|
Webhooks do nothing but /health answers | Likely a signature mismatch (different secret) or the event failed events/filters — you get 200 with "ignored" |
| Port 8644 is taken | Something else uses it; change WEBHOOK_PORT and update the source URL |
| The agent stops reacting after a restart | The gateway serves webhooks — it must be running (hermes gateway status) |
| Too many runs from one PR | Add coalesce (key e.g. pull_request.number) |
| You only want to forward a message | deliver_only: true — no agent, no model cost |
