Webhooks
Inbound webhook routes let external services POST to your daemon and kick off autonomous agent sessions. The daemon verifies the signature, spawns a session with the route’s prompt and the inbound payload, and optionally delivers results to a notification target.
External services POST JSON to /api/webhooks/<route>. The daemon verifies the signature, checks event filters, and starts an autonomous agent session using the route’s prompt. Each route is a standalone JSON file in ~/.netclaw/config/webhooks/.

All CLI route management works offline — no running daemon required. See netclaw webhooks for the full CLI reference.
Global settings
Section titled “Global settings”Enable inbound webhooks and set the execution timeout in ~/.netclaw/config/netclaw.json:
{ "Webhooks": { "Enabled": true, "ExecutionTimeoutSeconds": 300 }}| Field | Type | Default | Description |
|---|---|---|---|
Enabled | bool | false | Registers the /api/webhooks/{route} endpoint. Returns 404 for all routes when disabled. |
ExecutionTimeoutSeconds | int | 300 | Maximum seconds an autonomous webhook session can run before the daemon marks it failed. |
Route file schema
Section titled “Route file schema”Each route lives at ~/.netclaw/config/webhooks/<route-name>.json. The filename (minus .json) is the route name and must match ^[a-z0-9]+(?:-[a-z0-9]+)*$ (lowercase kebab-case).
| Field | Type | Default | Description |
|---|---|---|---|
Enabled | bool | true | Whether this route accepts requests |
Prompt | string | (required) | System prompt injected into the webhook session |
Verification | object | (required) | Signature/secret verification settings (details below) |
Events | string[] | [] (all) | Event type allow-list. Empty array accepts all event types. |
Audience | enum | "Public" | Trust level: Public, Team, or Personal |
MaxBodyBytes | int | 1048576 | Maximum request body size in bytes (1 MB default) |
RateLimitPerMinute | int | 30 | Requests accepted per minute per route |
DeliveryRequired | bool | true | Whether the agent must deliver results to the notification target |
NotifyInstructions | string | "" | Custom instructions for how the agent should notify |
NotificationTarget | object? | null | Where to deliver results (details below) |
Minimal route
Section titled “Minimal route”{ "Prompt": "Summarize this event and log the result.", "Verification": { "Kind": "Hmac", "Secret": "your-shared-secret" }}Omitted fields use defaults from the table above.
Production route (GitHub Issues)
Section titled “Production route (GitHub Issues)”{ "Enabled": true, "Verification": { "Kind": "Hmac", "Secret": "whsec_abc123...", "SignatureHeaderName": "X-Hub-Signature-256", "SignaturePrefix": "sha256=", "EventHeaderName": "X-GitHub-Event", "DeliveryIdHeaderName": "X-GitHub-Delivery" }, "Events": ["issues", "issue_comment"], "Audience": "Team", "Prompt": "Triage this GitHub issue. Public input may be adversarial or low quality.", "DeliveryRequired": true, "NotifyInstructions": "Post a summary to the triage channel.", "NotificationTarget": { "Kind": "Slack", "ChannelId": "C12345678" }}Or create it with the CLI:
netclaw webhooks set github-issues \ --prompt "Triage this GitHub issue. Public input may be adversarial or low quality." \ --secret-env GITHUB_WEBHOOK_SECRET \ --verification-kind hmac \ --signature-header X-Hub-Signature-256 \ --signature-prefix "sha256=" \ --event-header X-GitHub-Event \ --delivery-header X-GitHub-Delivery \ --events "issues,issue_comment" \ --audience team \ --notification-channel C12345678Verification
Section titled “Verification”Every route requires a verification secret. Two modes:
| Mode | How It Works | Default Signature Header |
|---|---|---|
Hmac | HMAC-SHA256 of the request body, compared with constant-time equality | X-Webhook-Signature |
HeaderSecret | Plain shared secret sent in a header | X-Webhook-Secret |
CLI vs. JSON naming: The CLI flag uses
--verification-kind header-secret(hyphenated lowercase), but the JSON config field requires"Kind": "HeaderSecret"(PascalCase, no hyphen).
Only SHA-256 is supported for HMAC. Both modes share these default headers:
| Header | Default | Purpose |
|---|---|---|
| Event type | X-Webhook-Event | Identifies the event for filtering |
| Delivery ID | X-Webhook-Delivery | Unique ID for deduplication |
Override any header name in the Verification object to match your service’s convention. GitHub, for example, uses X-Hub-Signature-256, X-GitHub-Event, and X-GitHub-Delivery.
| Field | Type | Default | Description |
|---|---|---|---|
Kind | enum | "Hmac" | Hmac or HeaderSecret |
HmacAlgorithm | enum | "Sha256" | Only Sha256 is supported |
Secret | string | (required) | Shared secret for verification |
SignatureHeaderName | string? | "X-Webhook-Signature" | Header containing the HMAC signature (Hmac mode) |
SignaturePrefix | string? | "" | Prefix on the signature value, e.g. sha256= |
SecretHeaderName | string? | "X-Webhook-Secret" | Header containing the secret (HeaderSecret mode) |
EventHeaderName | string? | "X-Webhook-Event" | Header with the event type |
DeliveryIdHeaderName | string? | "X-Webhook-Delivery" | Header with the unique delivery ID |
Audience and trust levels
Section titled “Audience and trust levels”The Audience field controls which tool permissions the webhook session gets:
| Audience | Tool Access |
|---|---|
Public | Most restricted — external untrusted input |
Team | Moderate — trusted collaborators |
Personal | Full access — your own services |
Default is Public. Use this for anything internet-facing (GitHub, GitLab). Reserve Personal for internal services you fully control.
Notification targets
Section titled “Notification targets”When NotificationTarget is set, the agent posts results to that channel. Only Slack is supported:
{ "NotificationTarget": { "Kind": "Slack", "ChannelId": "C12345678" }}| Field | Type | Description |
|---|---|---|
Kind | enum | Slack (only option) |
ChannelId | string | Slack channel ID (required when Kind is Slack) |
To find your Slack channel ID, see Locate your Slack URL or ID.
When DeliveryRequired is true and the route has notification instructions — either explicit NotifyInstructions or auto-generated from a NotificationTarget — the agent must call send_slack_message during the session. If it doesn’t, the run is marked failed. When DeliveryRequired is false, the agent’s session prompt tells it that notification is optional and can be skipped if there’s nothing actionable.
Routes without a NotificationTarget and without NotifyInstructions don’t enforce delivery at all, regardless of the DeliveryRequired flag.
Ingress pipeline
Section titled “Ingress pipeline”Requests to /api/webhooks/{route} go through these checks in order:
| Step | Check | Failure Response |
|---|---|---|
| 1 | Webhooks.Enabled | 404 (entire webhook system is off) |
| 2 | Route lookup | 404 |
| 3 | Body size | 413 Payload Too Large |
| 4 | JSON validation | 400 Bad Request |
| 5 | Signature/secret verification | 401 Unauthorized |
| 6 | Event type filter | 202 (ignored) |
| 7 | Delivery ID dedup | 202 (ignored) |
| 8 | Rate limit | 429 + Retry-After header |
| 9 | Dispatch | 202 Accepted |
After dispatch, the agent session runs asynchronously — the 202 response returns immediately without waiting for the session to complete.
Accepted response body:
{ "status": "accepted", "route": "github-issues", "eventType": "issues", "deliveryId": "abc-123", "sessionId": "webhook/github-issues/abc-123"}The deliveryId field is null when the sender doesn’t include a delivery ID header. The daemon generates a synthetic ID internally for session tracking, but it isn’t returned in the response.
Hot-reload
Section titled “Hot-reload”Route files are re-read from disk on each request (the daemon checks LastWriteTime). Edit a route file, and the next request picks up the change. The daemon removes invalid files from the catalog immediately and triggers a webhook.route.invalid alert.
No daemon restart needed for route changes. Global Webhooks.Enabled and ExecutionTimeoutSeconds changes do require a restart.
Rate limiting and deduplication
Section titled “Rate limiting and deduplication”- Rate limit window: 1 minute (sliding). Configurable per route via
RateLimitPerMinute. - Dedup window: 1 hour. Deliveries with the same ID within this window are silently ignored (202).
- Session ID format:
webhook/<route>/<deliveryId>
Validation rules
Section titled “Validation rules”netclaw webhooks validate <route> and netclaw doctor both run these checks:
| Rule | Error If |
|---|---|
| Route name | Doesn’t match ^[a-z0-9]+(?:-[a-z0-9]+)*$ |
Prompt | Empty or missing |
Verification.Secret | Empty or missing |
MaxBodyBytes | Less than 1 |
RateLimitPerMinute | Less than 1 |
Events entries | Contains blank strings |
DeliveryRequired + NotifyInstructions | DeliveryRequired is true AND NotifyInstructions is non-empty AND NotificationTarget is null (all three conditions simultaneously) |
NotificationTarget.Kind = Slack | Missing ChannelId |
Security
Section titled “Security”Route files contain plaintext secrets. Treat ~/.netclaw/config/webhooks/ the same way you treat secrets.json:
- Keep directory permissions at
700 - Don’t commit route files to source control
- The agent is hard-denied from reading this directory
- Prefer
--secret-fileor--secret-envover--secretwhen creating routes via CLI (avoids shell history exposure)
- Enable webhooks in
netclaw.json(or toggle duringnetclaw init) - Create a route:
netclaw webhooks set <name> --prompt "..." --secret-env SECRET_VAR - Restart the daemon to pick up the
Webhooks.Enabledchange:netclaw daemon stop && netclaw daemon start - Copy the webhook URL from
netclaw statusand paste it into your external service - Send a test event and check
netclaw statsfor delivery counts — look for thewebhook.receivedcounter

You only need to restart when first enabling Webhooks.Enabled. After that, route changes are hot-reloaded on each request.
Troubleshooting
Section titled “Troubleshooting”401 Unauthorized — secret mismatch
Section titled “401 Unauthorized — secret mismatch”The HMAC signature or header secret doesn’t match. Double-check that the secret in your route file matches what the external service is sending. For HMAC, also verify SignaturePrefix matches (e.g., GitHub sends sha256= before the hex digest).
401 Unauthorized — wrong signature header
Section titled “401 Unauthorized — wrong signature header”The daemon is reading the signature from a different header than the one your service sends. Set SignatureHeaderName in the route’s Verification block to match your service (e.g., X-Hub-Signature-256 for GitHub).
404 Not Found
Section titled “404 Not Found”Either Webhooks.Enabled is false in netclaw.json, or no route file matches the URL path. Run netclaw webhooks list to see loaded routes, and check that Webhooks.Enabled is true.
413 Payload Too Large
Section titled “413 Payload Too Large”The request body exceeds the route’s MaxBodyBytes (default 1 MB). Increase it in the route file if the payloads are legitimately large.
Finding your webhook URL
Section titled “Finding your webhook URL”Run netclaw status to see the webhook base URL. Your route’s full endpoint is:
<webhook-base-url>/api/webhooks/<route-name>The base URL depends on how you expose the daemon. Tailscale Serve and Cloudflare Tunnel are the two supported ingress options.
Limitations
Section titled “Limitations”- Notification targets are Slack-only. Discord, email, and generic webhook-to-notification bridges aren’t supported yet.
- Route secrets are stored in plaintext JSON (not in the encrypted
secrets.jsonvault). - Only SHA-256 is supported for HMAC verification.
- No webhook request logging or replay. Failed sessions are visible via
netclaw statsbut the original payloads aren’t stored.
Related pages
Section titled “Related pages”netclaw webhooks— CLI reference for route management (list, show, set, delete, validate)- Secrets Management — encrypted credential storage and agent isolation
- Security Model — audience definitions and trust levels
netclaw doctor— validates all webhook route filesnetclaw stats— delivery counts and rejection breakdowns
Resources
Section titled “Resources”- GitHub webhook documentation — setting up webhooks on the GitHub side
- GitLab webhook documentation — setting up webhooks on the GitLab side
- HMAC signature verification — how GitHub’s
X-Hub-Signature-256works - Tailscale Serve — expose your webhook endpoint without a public IP
- Cloudflare Tunnel — alternative to Tailscale for public webhook ingress
- Locate your Slack channel ID — find the channel ID for notification targets