Webhook alerts

Configure Slack, PagerDuty, Webex, and generic HMAC webhooks so the gateway can ping a chat or incident channel when high-severity events fire.

defenseclaw setup webhook manages the notifier webhooks the gateway calls when an event meets your minimum severity. Four types are supported. Each one can be set up with prompts or with flags only.

Notifiers and telemetry destinations

There are two separate webhook surfaces:

  • Notifiers (defenseclaw setup webhook) — chat/incident pings shaped to the destination's API (Slack blocks, PagerDuty Events v2, Webex messages, or generic JSON). Filtered by severity and deduplicated. This page.
  • HTTP JSONL telemetry destinations (defenseclaw setup observability add webhook) — canonical v8 log projections posted as JSON Lines. Omitted policy selects every collected log bucket; send/routes and destination redaction can narrow it. It has an independent bounded queue and is intended for SIEMs and data lakes.

Pick the surface that matches the consumer. Most installs run one notifier (Slack) and zero or more independent telemetry destinations.

The four webhook types

Prop

Type

Three worked examples

Slack incoming webhooks carry the secret in the URL itself, so you do not need a --secret-env:

defenseclaw setup webhook add slack \
  --name slack-alerts \
  --url https://hooks.slack.com/services/T000/B000/XXXXXX \
  --min-severity HIGH \
  --non-interactive

The name is derived from the type and host. Test it without posting to the channel by using --dry-run, then send a real test when you're happy:

defenseclaw setup webhook test slack-alerts --dry-run
defenseclaw setup webhook test slack-alerts

PagerDuty Events v2 needs a routing key. Store it with keys set (hidden prompt, saved to ~/.defenseclaw/.env) and point the webhook at the env var's name:

defenseclaw keys set DEFENSECLAW_PD_ROUTING_KEY
defenseclaw setup webhook add pagerduty \
  --name pd-prod \
  --url https://events.pagerduty.com/v2/enqueue \
  --secret-env DEFENSECLAW_PD_ROUTING_KEY \
  --min-severity HIGH \
  --non-interactive

The gateway reads the key from its environment, which includes ~/.defenseclaw/.env, when it starts. After you rotate the key, run defenseclaw-gateway restart.

For SIEM or SOAR platforms that accept a generic POST with a signature. Store the shared secret in DEFENSECLAW_SIEM_SECRET first:

defenseclaw keys set DEFENSECLAW_SIEM_SECRET
defenseclaw setup webhook add generic \
  --name siem \
  --url https://siem.example.com/defenseclaw \
  --secret-env DEFENSECLAW_SIEM_SECRET \
  --events block,guardrail \
  --min-severity MEDIUM \
  --non-interactive

The receiver validates the signature by recomputing the hex HMAC-SHA256(secret, body) and comparing it to the X-Hub-Signature-256 header, which has the form sha256=<hex>.

A dry run shows the payload and the headers without sending anything:

defenseclaw setup webhook test siem --dry-run
  Testing webhook siem [generic] -> https://siem.example.com/***
  (dry-run) formatting only, no delivery
    Payload:        311 bytes
    Preview:        {"webhook_type":"defenseclaw_enforcement","defenseclaw_version":"1.0","event":{"id":"synthetic-8fd159624404","timestamp":"2026-10-03T04:09:51Z","action":"webhoo
    Headers:
      Content-Type: application/json
      User-Agent: defenseclaw-cli/webhook-test
      X-Hub-Signature-256: <redacted>
    OK Result:         dry-run OK

A connector-hook block names the rule that fired. The event's details carry rule=<id> next to the redacted reason, and defenseclaw_rule gives the same label the agent sees, for example rule VB2-MARKER-BLOCK: Marker command. Titles appear only for compiled-in and loaded rule-pack rules; the matched content stays redacted.

All seven subcommands

Prop

Type

Flag reference for add

FlagDefaultNotes
--name <id>derived from type + URL hostFriendly identifier shown in list and audit.
--url <url>(required)Validated against the SSRF guard at write time and again at delivery.
--secret-env <ENV>required for pagerduty and webexThe name of an env var, never the literal secret. It must look like an env var name (A-Z, 0-9, _).
--room-id <id>(required for webex)Webex room/space identifier.
--min-severity INFO|LOW|MEDIUM|HIGH|CRITICALHIGHEvents below the floor are dropped. Case-insensitive.
--events a,b,c(all categories)Allow-list of event categories: block, scan, guardrail, drift, health.
--timeout-seconds <n>10Per-delivery timeout.
--cooldown-seconds <n>300 (runtime default)Dedup window. 0 disables dedup; omit to use the runtime default.
--enabled / --disabled--enabledInitial state.
--connector <name>(global)Scope the webhook to one connector. That connector's events go to its own webhooks when it has any, and to the global webhooks otherwise. list and remove take the same option.
--dry-runoffPrint the YAML diff without writing.
--non-interactiveoffRequired for CI; a missing required field exits non-zero with a message such as error: pagerduty: --secret-env is required.

Env var names the interactive prompt suggests for --secret-env:

TypeSuggested --secret-env
slack(none — secret is in the URL)
pagerdutyDEFENSECLAW_PD_ROUTING_KEY
webexDEFENSECLAW_WEBEX_TOKEN
genericasks whether to sign with HMAC first, then suggests DEFENSECLAW_WEBHOOK_SECRET

SSRF guard

Every URL is checked against the same SSRF guard the gateway runs at delivery time:

  • The scheme is http:// or https://, and the host parses.
  • The host resolves to a public address. Private (RFC 1918 and IPv6 ULA), link-local, loopback, cloud-metadata and carrier-grade NAT (100.64.0.0/10) addresses are rejected.
  • DEFENSECLAW_WEBHOOK_ALLOW_LOCALHOST=1 allows localhost and loopback addresses only. DEFENSECLAW_ALLOW_CGNAT=1 allows the 100.64.0.0/10 range only. Every other private range stays blocked.
  • No credentials in the URL (https://user:pass@…); they belong in --secret-env.
A private address is refused at add time
error: IP 10.0.0.5 is a private or reserved address; webhooks must reach a public address so events can't be sent to internal services (SSRF guard). Use a public endpoint or relay.

The check runs at add time and again at delivery time, which catches hand edits to config.yaml.

Verifying a webhook with test

test sends a synthetic event with a new event ID each time, so receivers do not deduplicate it:

defenseclaw setup webhook test pd-prod

The output includes:

  • The formatted payload bytes count and a 160-byte preview.
  • Every header DefenseClaw sent; the HMAC signature for generic is shown as <redacted>.
  • The HTTP status returned by the receiver.
  • A setup-webhook audit event with name=pd-prod type=pagerduty ok=True, recorded when the gateway is running.

To format the payload without sending it:

defenseclaw setup webhook test pd-prod --dry-run

Common gotchas

See also

  • Webhook CLI source (cmd_setup_webhook.py): every subcommand, validator and helper.
  • internal/gateway/webhook.go — the dispatcher, dedup logic, and HMAC implementation.
  • Reference → CLI — the CLI command catalog.
  • Observability → Splunk — to export every event, with no severity floor, as a telemetry destination.