Webhook Events is a capability that turns verified external events into DSH Sessions: an authenticated provider delivery (for example, a signed GitHub webhook) reaches a trusted programmatic rule, and a rule may create an ordinary root Session inside a Web Workspace to act on the event. Dispatch is process-local and fire-and-forget — there is no delivery database, queue, retry, deduplication, or Agent-completion state. The family is entirely user-composed: no shipped bundle wires it in.
| Package | Role | ctx key |
|---|---|---|
packages/webhook/webhook | Rule registry, callback lifecycle, and Workspace-backed Session creation | ctx.webhookRuntime |
packages/webhook/webhook-github | Signed GitHub HTTP adapter (exact WebServer route, bounded body, HMAC-verified) | consumes ctx.webhookRuntime + ctx.webServer |
The split follows the seam pattern used across dsh capabilities: provider adapters authenticate and normalize deliveries, rules own arbitrary conditions and external calls, and the runtime owns callback lifetime plus Session creation. This is a different mechanism from packages/hooks/* (the Claude Code / Codex shell-hook bridges), which fire pre-existing hook configs during agent runs rather than starting new Sessions from external events.
The runtime: WebhookRuntime
ctx.webhookRuntime (packages/webhook/webhook/src/index.ts) is a Host-side registry for trusted programmatic rules with exactly two operations:
register<K extends string>(rule: WebhookRule<K>): () => Promise<void>
dispatch<K extends string>(delivery: VerifiedWebhookDelivery<K>): voidWebhookRule<K> has a branded unique id, a provider kind, and run(delivery, signal). The callback may execute arbitrary trusted code and returns either null or one WebhookSessionRequest:
interface WebhookRule<K extends string> {
readonly id: WebhookRuleId
readonly kind: K
run(delivery: VerifiedWebhookDelivery<K>, signal: AbortSignal):
Promise<WebhookSessionRequest | null> | WebhookSessionRequest | null
}VerifiedWebhookDelivery<K> carries the provider kind, configured source, provider deliveryId, normalized lossless JSON event, and non-negative safe-integer receivedAt. The runtime validates, detaches, and freezes the complete value before dispatching it to more than one rule. deliveryId is provenance only — the runtime neither stores nor deduplicates it, so a repeated delivery runs the rules again.
dispatch() snapshots the matching rules, schedules each independently, and returns before any callback settles; throws and rejections are contained per rule. Registration is an effect whose awaitable disposer first hides the rule, then aborts and drains active callbacks — callbacks must observe the supplied signal, because same-process code that ignores cancellation cannot be forcibly stopped safely.
Session request
A non-null WebhookSessionRequest requires an absolute workspacePath, title, prompt, agentPreset, and permissionPreset. Optional model names an explicit provider/model route plus an output-token cap:
interface WebhookSessionRequest {
readonly workspacePath: string // absolute; creates a root Session inside a Web Workspace
readonly title: string
readonly prompt: string // exactly what the model sees — no private framing
readonly agentPreset: string
readonly permissionPreset: string
readonly model?: { provider: string; model: string; maxTokens?: number }
}An explicit route uses its adapter's reasoning default; omission snapshots the complete current deployment selection, including reasoning effort, until the first request records its durable header. The runtime validates presets before mutation, resolves or creates the canonical Workspace, creates an Agent with that Workspace path as SessionHeader.cwd, mounts the agent preset before publication, and durably attaches the Session before applying permissions, title, and prompt. Successful Agent.followup() is the webhook operation's commit point — the message uses source.kind: "webhook" with provider, source, delivery, and rule provenance. The runtime does not wait for idle, inspect the reply, or publish completion state; ordinary Agent and Session behavior owns everything afterward.
The GitHub adapter
@deepseek-ai/dsh-webhook-github registers one exact route (kind: 'exact') on an injected WebServer (packages/webhook/webhook-github/src/index.ts). Per request it resolves its credential reference, reads the raw body under a bounded maxBodyBytes ceiling, and verifies the X-Hub-Signature-256 HMAC before any JSON parsing — a body that never reaches the parser cannot be trusted:
| Check | Order | Failure |
|---|---|---|
POST only | before body read | 405 + Allow: POST |
Content-Type: application/json (one optional UTF-8 charset) | before body read | 415 |
Content-Length / streamed bytes ≤ maxBodyBytes | during body read | 413 |
X-Hub-Signature-256, x-github-delivery, x-github-event headers present | after body read | 400 |
| HMAC verification against the resolved secret | before JSON parse | 401 |
| Lossless JSON object payload | after verification | 400 |
Dispatch to ctx.webhookRuntime | last | 503 if unavailable |
On success the adapter answers 202 immediately after in-memory dispatch — it never waits for rules or Sessions to settle. The normalized event is a signed lossless-JSON object with { name: eventName, payload }, delivered as VerifiedWebhookDelivery<'github'>; this is provider-neutral delivery, and rules validate the event-specific fields they consume. A 503 is also returned when the webhook secret credential is unavailable.
The GitHub review guide (docs/user/guide/github-review.md) mounts this route on an isolated second WebServer so exposing webhook ingress never exposes the browser API. The shipped example overlay is apps/cli/config/examples/github-review/cordis.yml:
# Opt-in GitHub webhook overlay over the shipped Web composition. The second
# WebServer lives in an isolated realm so exposing it never exposes the UI API.
- insert:
- id: webhook-runtime
name: '@deepseek-ai/dsh-webhook'
- id: github-ready-review-rule
name: './github-ready-review-rule.mjs'
config:
source: primary-github
repository: deepseek-harness/deepseek-harness
workspacePath: !!js process.env.DSH_GITHUB_REVIEW_WORKSPACE ?? process.cwd()
agentPreset: standard
permissionPreset: read-only
- id: github-webhook-ingress
name: cordis:group
group: true
isolate:
webServer: true
config:
- id: github-webhook-server
name: '@deepseek-ai/dsh-host-webserver'
config:
host: '127.0.0.1'
port: !!js Number(process.env.DSH_GITHUB_WEBHOOK_PORT ?? 3081)
- id: github-webhook-adapter
name: '@deepseek-ai/dsh-webhook-github'
config:
source: primary-github
path: /github
secretEnv: DSH_GITHUB_WEBHOOK_SECRET
maxBodyBytes: 1048576The rule module (github-ready-review-rule.mjs) is where the composition's logic lives: it inspects a delivery, decides whether to act, and returns a WebhookSessionRequest or null.
What fire-and-forget does not guarantee
These caveats are the contract's honest envelope — acceptance is not Agent success:
- Process-local fire-and-forget only — a crash loses rule calls that have not admitted a prompt; there is no queue, replay, or retry.
- No built-in deduplication — repeated provider deliveries may create repeated Sessions; rules that need idempotency own it.
- No completion result — HTTP acceptance and rule settlement do not report Agent success, idle, or output.
- Trusted callbacks must cooperate with cancellation — runtime teardown aborts and awaits them but cannot terminate arbitrary same-process code.
- Workspace creation may outlive a failed Session attempt — an empty Workspace is retained because another concurrent caller may already use it.
Because none of this is wired into a shipped bundle, a deployment composes the runtime, adapter, and rule modules explicitly — including from a source checkout.
Packages
| Package |
|---|
@deepseek-ai/dsh-webhook |
@deepseek-ai/dsh-webhook-github |
Further reading
packages/webhook/README.md— the webhook family package mappackages/webhook/webhook/README.md— the rule interface and Session request contractpackages/webhook/webhook-github/README.md— the GitHub adapter surfacedocs/subsystems/webhook.md— shared types and timing guaranteesdocs/user/guide/github-review.md— the full composed GitHub review exampleapps/cli/config/examples/github-review/cordis.yml— the shipped opt-in overlay