The webhook receiver
The GitHub webhook receiver: verifies deliveries, maps each to a work item, and routes it to the harness session working that item.
Its lifecycle is the-loop start|stop|status — it comes up when webhooks.ghWebhook.enabled is true (issue-228; there is no gh-webhook command any more). By default it runs inside the service process (service.hostIngresses, issue-231), still listening on its own configured port and holding its own pidfile lock; with hostIngresses: false (or the service disabled) it is spawned as its own process, and python -m the_loop.daemon_entry gh-webhook runs it in the foreground for a systemd Type=simple unit. Everything it listens with — host, port, path, the secret's env var, the event filter — is webhooks.ghWebhook in the CLI config; whether it dispatches at all is routing.enabled.
Verification
The X-Hub-Signature-256 HMAC is verified whenever the secret env var is set:
export THE_LOOP_GH_WEBHOOK_SECRET='the same secret you gave GitHub'
the-loop startThe secret is read from the environment, never from a flag, so it cannot leak into a process listing — and never from the config file, so it cannot be committed.
Unset means unverified
With the variable unset the receiver starts, warns, and accepts unsigned deliveries. Anyone who can reach the port can post an event.
GET /health returns 200 ok unconditionally — use it for a readiness probe.
Routing
With routing.enabled, each verified event is mapped to the work item(s) it concerns and delivered to that item's registered session:
- Extraction — issue/PR number, the
issue-<n>PR head-branch convention, closing keywords, and the PRs behindworkflow_run/check_*events. - Dedup — on
X-GitHub-Delivery, through a bounded LRU (dedupCacheSize), so GitHub's redeliveries are processed at most once. - Dispatch — the rendered prompt is pasted into the matched session's tmux-hosted TUI (respawning it first when it has died), one event at a time per session, in parallel across sessions (
maxConcurrentDispatches). - Unmatched events follow
spawnOnUnmatched.
Design: docs/specs/issue-15/design.md · decision-016.
Guards
Both run before dispatch, in this order. See concepts.
Self-reply guard
the-loop posts under your own credentials, so authorship cannot distinguish its comments from yours. Every comment, review and reply it writes carries an embedded marker, and a marker-carrying event is dropped before dispatch regardless of actor — so the-loop's own reply never resumes the session that wrote it. decision-031.
Authorized-actor guard
Required, no fallback, fails closed
The receiver acts only on actions by logins in routing.authorizedUsers — CLI config only, with no fallback to any repository's harness config. Comments, reviews, and issue/PR labels and opens from anyone else are dropped before dispatch. An empty list fails closed, with a warning at startup.
CI and system events, which carry no human instructions, still pass; and a closed event still auto-closes that item's own session. Each operator runs their own instance for their own logins. decision-023.
Execution control
A comment carrying a declared control keyword is interpreted by the-loop and not forwarded to the agent. Parsing happens strictly after both guards, so it is never a second, weaker way in. With the default requireStartCommand: true, a labelled work item waits for an authorized user's explicit start before anything spawns.
Config hot-reload
While the receiver runs, edits to routing and events are picked up on the next received event — no restart. The soft policy swaps live: events filter, label, spawn policy, harness, per-harness args, prompt templates. The dedup cache, per-session queues and registry are preserved.
Infrastructural settings still need a restart: host, port, path, secretEnv, maxConcurrentDispatches, dedupCacheSize, registryDir, webTerminal. An invalid edit is logged and the previous config kept.
Observability
Every receive, reject, route, dispatch, spawn and close decision is appended to the event log. Query it with the-loop events --source gh-webhook.
See also
- Webhook options · Routing options
- polling — the same dispatch stack, pull-based; started by the same
the-loop start. - webhook triggers — the capability doc.