hermes - ✅(Solved) Fix feat(gateway): per-route sender and event-type denylists for webhook routes [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#18041Fetched 2026-05-01 05:54:13
View on GitHub
Comments
1
Participants
1
Timeline
6
Reactions
0
Participants
Timeline (top)
labeled ×4commented ×1cross-referenced ×1

Fix Action

Fix / Workaround

  • ignored_event_types — list of event-type strings to drop without dispatching the agent. Case-insensitive. Applied after the events allowlist so existing route configs are unaffected.
  • ignored_senders — list of sender.login values to drop. Case-insensitive exact match. Payloads without a sender.login are not filtered (defensive default — the field isn't formally guaranteed on every Forgejo/Gitea event type).

PR fix notes

PR #18042: feat(gateway): per-route sender and event-type denylists for webhook routes

Description (problem / solution / changelog)

Closes #18041.

Summary

Adds optional ignored_senders and ignored_event_types keys to webhook route config so a system-wide Forgejo/Gitea/GitHub firehose can drop noisy events (status, push, dependabot, etc.) without invoking the agent. Also recognizes X-Gitea-Event and X-Forgejo-Event headers alongside the existing GitHub/GitLab detection.

Filtered events return 200 {filtered: true, reason, ...} so the source treats the request as successful and won't retry. Both new keys default to empty — fully backward-compatible.

Example

platforms:
  webhook:
    extra:
      routes:
        forgejo-firehose:
          secret: ${FORGEJO_WEBHOOK_SECRET}
          ignored_event_types: [status, push, watch, star]
          ignored_senders: [dependabot, forgejo-actions, mirror-sync-bot]
          prompt: ...

Design notes

  • Allowlist still wins. The existing events: allowlist runs first (returning {status: ignored}), then the new denylists run (returning {filtered: true, ...}). This preserves backward compatibility and gives different response shapes for "config rejected this" vs "denylist filtered this," which helps when debugging overlapping configs.
  • Sender extraction is defensive. sender.login isn't formally documented as present on every Forgejo/Gitea event type, so a missing/non-dict sender falls through to dispatch rather than getting silently dropped.
  • Case-insensitive exact match. No glob/regex (YAGNI). Easy to add later if needed.
  • Filter placement. At the existing event-type filter seam (after auth, signature validation, parse, rate-limit). Keeps the change minimally invasive. One known consequence: filtered events count against the route's rate limit; the lever is the rate_limit config if it bites.

Test plan

  • 9 new unit tests in tests/gateway/test_webhook_adapter.py::TestDenylistFilters — both filters, case-insensitivity, missing-sender defensive default, allowlist precedence, Forgejo/Gitea header detection, backward compat
  • All 45 webhook-adapter tests pass; 27 related webhook integration/dynamic-route/signature tests still pass
  • Tested on Linux (Fedora 43, Python 3.12). Pure dict/string logic — no platform-specific behavior, no file I/O, no process management
  • Manual: configure a route with denylists, fire curl with various Forgejo payloads, confirm 200 {filtered: true} and no agent dispatch on hits, normal dispatch on misses

Out of scope (intentional)

  • Glob/regex sender matching — exact match covers the stated use case
  • Reordering the rate-limit step relative to the filter — keeping the change minimal
  • Startup-time schema validation for unknown route keys (e.g. typo'd ignored_sender singular silently no-ops) — worth a separate small PR

Files

  • gateway/platforms/webhook.py — header detection (Gitea/Forgejo) + two denylist checks at the existing filter seam
  • tests/gateway/test_webhook_adapter.pyTestDenylistFilters class with 9 tests
  • website/docs/user-guide/messaging/webhooks.md — route-properties table updated; filter response shape documented

🤖 Generated with Claude Code

Changed files

  • gateway/platforms/webhook.py (modified, +43/-0)
  • tests/gateway/test_webhook_adapter.py (modified, +226/-0)
  • website/docs/user-guide/messaging/webhooks.md (modified, +5/-1)

Code Example

platforms:
  webhook:
    extra:
      routes:
        forgejo-firehose:
          secret: \${FORGEJO_WEBHOOK_SECRET}
          ignored_event_types: [status, push, watch, star]
          ignored_senders: [dependabot, forgejo-actions, mirror-sync-bot]
          prompt: ...
RAW_BUFFERClick to expand / collapse

Problem

When pointing a system-wide webhook source at Hermes — e.g. a Forgejo (or Gitea) instance configured to forward all webhooks to a single route — the agent receives a high volume of low-value events that should not trigger an LLM run:

  • Per-commit push events
  • CI status and check_run updates
  • Bot-driven activity (dependabot, renovate, mirror-sync bots, Forgejo Actions)
  • watch / star churn
  • Repository create/delete from auto-provisioning

Today the only filter is the per-route events: allowlist, which is awkward when the goal is "accept everything except the noise." You'd have to enumerate every event type you do want, and keep that list in sync as Forgejo/Gitea/GitHub add new events.

There is also no way to drop events based on who sent them, so bot-generated traffic always reaches the agent.

Proposal

Add two optional per-route config keys, alongside the existing events allowlist:

  • ignored_event_types — list of event-type strings to drop without dispatching the agent. Case-insensitive. Applied after the events allowlist so existing route configs are unaffected.
  • ignored_senders — list of sender.login values to drop. Case-insensitive exact match. Payloads without a sender.login are not filtered (defensive default — the field isn't formally guaranteed on every Forgejo/Gitea event type).

While we're here, also recognize the X-Gitea-Event and X-Forgejo-Event headers in the existing event-type detection chain (currently only X-GitHub-Event and X-GitLab-Event are checked). Forgejo/Gitea webhook payloads are largely GitHub-compatible, but the header name differs and Hermes was missing the detection.

Example

platforms:
  webhook:
    extra:
      routes:
        forgejo-firehose:
          secret: \${FORGEJO_WEBHOOK_SECRET}
          ignored_event_types: [status, push, watch, star]
          ignored_senders: [dependabot, forgejo-actions, mirror-sync-bot]
          prompt: ...

Behavior

  • Filtered events return HTTP 200 with body {\"filtered\": true, \"reason\": \"event_type_denied\" | \"sender_denied\", ...} so the source treats the request as successful and won't retry.
  • Both new keys default to empty — fully backward-compatible with existing route configs.
  • Filter runs at the same seam as the existing events allowlist (after auth, signature validation, and rate-limit), so it doesn't change the security or rate-limiting posture.

Out of scope (intentional)

  • Glob/regex sender matching — exact match covers the stated use case; can be added later if needed.
  • Reordering the rate-limit step — keeping the change minimally invasive. One known consequence: filtered events still count against the route's rate limit. If a denylist-heavy firehose hits the limit, raising the route's rate_limit is the lever.
  • Schema validation for unknown route keys — a typo'd ignored_sender (singular) would silently no-op. Worth a separate small PR.

Implementation

I have a working implementation with full TDD coverage on a fork. Happy to open a PR — let me know if the design above looks right, or if you'd like changes (e.g. naming, semantics around allowlist+denylist interaction, filter placement) before I do.

extent analysis

TL;DR

To filter out low-value events and bot-generated traffic, add ignored_event_types and ignored_senders config keys to the webhook route configuration.

Guidance

  • Add ignored_event_types to drop specific event types, such as push, status, watch, and star, without dispatching the agent.
  • Add ignored_senders to drop events sent by specific senders, such as dependabot and mirror-sync-bot.
  • Recognize X-Gitea-Event and X-Forgejo-Event headers in the event-type detection chain to support Forgejo and Gitea webhooks.
  • Verify the filter by checking the HTTP response, which should return 200 with a JSON body indicating the event was filtered.

Example

platforms:
  webhook:
    extra:
      routes:
        forgejo-firehose:
          secret: ${FORGEJO_WEBHOOK_SECRET}
          ignored_event_types: [status, push, watch, star]
          ignored_senders: [dependabot, forgejo-actions, mirror-sync-bot]

Notes

The proposed solution is backward-compatible with existing route configurations and does not change the security or rate-limiting posture. However, filtered events will still count against the route's rate limit.

Recommendation

Apply the proposed workaround by adding ignored_event_types and ignored_senders config keys to the webhook route configuration, as it provides a targeted solution to filter out low-value events and bot-generated traffic.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix feat(gateway): per-route sender and event-type denylists for webhook routes [1 pull requests, 1 comments, 1 participants]