feat(webhooks): clerk webhooks toolkit (V1)#366
Conversation
🦋 Changeset detectedLatest commit: daacb49 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds the Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
.claude/rules/command-registration.md (1)
66-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAside omits the new
tokensubcommand.This PR registers three webhooks subcommands (
token,listen,verify), but the example here only mentionslistenandverify. Since this rules doc is authored alongside the command, consider listingtokentoo so the description stays accurate.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.claude/rules/command-registration.md at line 66, The webhooks group description is missing the new token subcommand, so update the guidance in the registerWebhooks example to include token alongside listen and verify. Keep the structure aligned with registerWebhooks by describing all three .command(...) entries and the inherited optsWithGlobals() options, while leaving preAction omitted since this group is auth-free.packages/cli-core/src/commands/webhooks/verify.ts (1)
113-118: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueCatch-all collapses all read failures to
FILE_NOT_FOUND.A permission error (EACCES), a directory path, or a decode failure on
Bun.file(path).text()would all surface as "File not found", which can mislead users diagnosing the real cause.♻️ Optionally preserve the underlying cause
try { return await Bun.file(path).text(); - } catch { - throw new CliError(`File not found: ${path}`, { code: ERROR_CODE.FILE_NOT_FOUND }); + } catch (err) { + const reason = err instanceof Error ? `: ${err.message}` : ""; + throw new CliError(`Could not read ${path}${reason}`, { code: ERROR_CODE.FILE_NOT_FOUND }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli-core/src/commands/webhooks/verify.ts` around lines 113 - 118, The read helper in verify.ts is mapping every failure from Bun.file(path).text() to FILE_NOT_FOUND, so update the catch path to preserve and classify the real error instead of treating all cases as missing files. In the function that reads the file, inspect the thrown error (for example permission, directory, or decode failures) and either rethrow with the original cause attached or translate it to a more accurate CliError code/message while keeping the underlying error details available.packages/cli-core/src/commands/webhooks/relay-client.ts (1)
120-131: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winHandle
onTokenRotatedrejection so the redial still fires.If
onTokenRotatedrejects (e.g.setRelayEntryfails to persist), the.then()never runs, so thesetTimeoutredial is never scheduled and the client silently stalls after a 1008 collision. The non-collision path at Line 134-135 reconnects unconditionally; the rotation path should be equally resilient.♻️ Schedule the redial regardless of persistence outcome
- void this.options.onTokenRotated(this.token).then(() => { - if (this.stopped) return; - setTimeout(() => this.connect(), RELAY_RECONNECT_DELAY_MS); - }); + void this.options.onTokenRotated(this.token).finally(() => { + if (this.stopped) return; + setTimeout(() => this.connect(), RELAY_RECONNECT_DELAY_MS); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli-core/src/commands/webhooks/relay-client.ts` around lines 120 - 131, The token-collision branch in relay-client’s reconnect flow only schedules the redial inside the onTokenRotated().then() success path, so a rejected persistence callback can stall reconnection. Update the collision handling in relay-client to use the same resilient redial behavior as the normal drop path: always schedule connect() after RELAY_RECONNECT_DELAY_MS once the token is rotated, and ensure any onTokenRotated failure is caught/logged without preventing the timeout from firing.packages/cli-core/src/lib/config.ts (1)
74-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional:
raw.relayis cast without validating entry shape.
typeof raw.relay === "object"also accepts arrays, and inner entries aren't checked to be{ token: string }. A hand-edited/corrupted config could surface a malformedRelayEntrytolistenwithout a usage error. Given this is local config, it's low risk — consider a light shape guard if you want defense in depth.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli-core/src/lib/config.ts` around lines 74 - 76, The raw relay config is being accepted too loosely in config.ts, since raw.relay can be an array and its entries are cast to RelayEntry without checking they match the expected shape. Tighten the parsing in the config loading logic by validating raw.relay is a plain object and confirming each relay entry has the expected token string before assigning to config.relay, so malformed local config cannot flow into listen unchecked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In @.claude/rules/command-registration.md:
- Line 66: The webhooks group description is missing the new token subcommand,
so update the guidance in the registerWebhooks example to include token
alongside listen and verify. Keep the structure aligned with registerWebhooks by
describing all three .command(...) entries and the inherited optsWithGlobals()
options, while leaving preAction omitted since this group is auth-free.
In `@packages/cli-core/src/commands/webhooks/relay-client.ts`:
- Around line 120-131: The token-collision branch in relay-client’s reconnect
flow only schedules the redial inside the onTokenRotated().then() success path,
so a rejected persistence callback can stall reconnection. Update the collision
handling in relay-client to use the same resilient redial behavior as the normal
drop path: always schedule connect() after RELAY_RECONNECT_DELAY_MS once the
token is rotated, and ensure any onTokenRotated failure is caught/logged without
preventing the timeout from firing.
In `@packages/cli-core/src/commands/webhooks/verify.ts`:
- Around line 113-118: The read helper in verify.ts is mapping every failure
from Bun.file(path).text() to FILE_NOT_FOUND, so update the catch path to
preserve and classify the real error instead of treating all cases as missing
files. In the function that reads the file, inspect the thrown error (for
example permission, directory, or decode failures) and either rethrow with the
original cause attached or translate it to a more accurate CliError code/message
while keeping the underlying error details available.
In `@packages/cli-core/src/lib/config.ts`:
- Around line 74-76: The raw relay config is being accepted too loosely in
config.ts, since raw.relay can be an array and its entries are cast to
RelayEntry without checking they match the expected shape. Tighten the parsing
in the config loading logic by validating raw.relay is a plain object and
confirming each relay entry has the expected token string before assigning to
config.relay, so malformed local config cannot flow into listen unchecked.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c28a1b71-572a-45b7-a233-fcaed0bf2e9c
📒 Files selected for processing (29)
.changeset/webhooks-listen-v1.md.claude/rules/command-registration.md.oxlintrc.jsonpackages/cli-core/src/cli-program.tspackages/cli-core/src/cli.tspackages/cli-core/src/commands/webhooks/README.mdpackages/cli-core/src/commands/webhooks/forward.test.tspackages/cli-core/src/commands/webhooks/forward.tspackages/cli-core/src/commands/webhooks/index.tspackages/cli-core/src/commands/webhooks/listen.test.tspackages/cli-core/src/commands/webhooks/listen.tspackages/cli-core/src/commands/webhooks/relay-client.test.tspackages/cli-core/src/commands/webhooks/relay-client.tspackages/cli-core/src/commands/webhooks/relay-protocol.test.tspackages/cli-core/src/commands/webhooks/relay-protocol.tspackages/cli-core/src/commands/webhooks/render.test.tspackages/cli-core/src/commands/webhooks/render.tspackages/cli-core/src/commands/webhooks/shared.tspackages/cli-core/src/commands/webhooks/token.test.tspackages/cli-core/src/commands/webhooks/token.tspackages/cli-core/src/commands/webhooks/verify.test.tspackages/cli-core/src/commands/webhooks/verify.tspackages/cli-core/src/lib/config.tspackages/cli-core/src/lib/errors.tspackages/cli-core/src/lib/gradient.test.tspackages/cli-core/src/lib/gradient.tspackages/cli-core/src/lib/input-json.test.tspackages/cli-core/src/lib/input-json.tspackages/cli-core/src/lib/signals.ts
|
!snapshot |
Snapshot failedThe snapshot publish workflow failed. View the workflow run for details. |
1 similar comment
Snapshot failedThe snapshot publish workflow failed. View the workflow run for details. |
clerk webhooks toolkit (V1)clerk webhooks toolkit (V1)
Ship the local webhooks toolkit that needs no Clerk backend, carved off main: - `clerk webhooks listen` — standalone Svix relay tunnel. Dials the relay, prints a stable inbox URL (token persisted under relay.__relay_only__ so it survives restarts; --token pins an explicit one), and forwards deliveries to --forward-to. No auth, no instance context, no signing secret, no HMAC. - `clerk webhooks verify` — offline HMAC-SHA256 signature verification from a saved listen event line or the four explicit header values. No network. Relay transport (relay-client/relay-protocol/forward/render) and verify are ported unchanged from the full webhooks branch; listen is rewritten to the relay-only path with all PLAPI coupling removed. Additive lib edits: relay config accessors, INVALID_WEBHOOK_SIGNATURE error code, and a named cliSigintHandler so listen can hand off SIGINT cleanly. Registered via the registrant pattern with no group auth gate. Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
…s group Ports the registrant-pattern rule from the full webhooks branch. The closing example is updated for this branch: `users` shows inherited group options via optsWithGlobals, and the auth-free `webhooks` group (listen + verify) shows a group that omits the preAction gate. Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
…roup help `clerk webhooks token` generates a valid relay token (c_ + 10 base62 chars) for `listen --token`, removing the guesswork of hand-writing the exact format. The bare token is the default stdout output (even non-interactively, so command substitution works) with `--json` opt-in; the usage hint is stderr-only and interactive-only so it never pollutes a pipe: clerk webhooks listen --token "$(clerk webhooks token)" The `webhooks` group now carries a 3-step examples block (token -> listen -> verify) shown when run solo or with --help. Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
- `listen --forward-to` is now a required option (forwarding is the point of the command); the examples and help reflect this. - When `listen` runs WITHOUT `--token`, the ready banner now warns that the auto-generated relay token isn't a guaranteed-stable handle (it can differ across machines, a cleared config, or a rare collision) and prints the exact `--token <c_…>` to pass next time to lock the current URL, plus a pointer to `clerk webhooks token`. Passing `--token` suppresses the warning. Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
…banner Surface https://dashboard.clerk.com/last-active?path=webhooks in the ready banner so users can jump straight to adding the relay URL as an endpoint. Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
expandInputJson auto-reads piped stdin as --input-json for any command in a non-TTY context. That stole stdin from `webhooks verify --payload -` / `--delivery -`, whose JSON keys were then mis-parsed into flags (e.g. "unknown option '--type'"). Skip the auto-stdin expansion when argv contains a bare `-`, so a command that explicitly claims stdin gets it. Verified: verify --payload - and --delivery - now succeed. Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
From a multi-agent adversarial sweep (27/28 findings confirmed). 14 fixes:
listen:
- validate --forward-to as our own usage error (JSON in agent mode) + reject
non-http(s)/invalid URLs at startup before opening the relay
- warn once when --headers carries a svix-* key (always overridden by delivery)
- emit a structured {"type":"reconnecting"} NDJSON line in agent mode
- drop endpoint_id/events_filter from the ready line (always null in V1)
- banner: remove internal "relay-only" jargon and the always-"all" events row
verify:
- --delivery accepts the full `listen --json` stream (skip the ready line)
- reject garbled/non-base64 secrets (Buffer.from silently truncates them)
- guard against --delivery - and --payload - both consuming stdin
- only show the clock-skew hint for structurally valid (32-byte v1) signatures
- hint when a --payload file has a trailing newline (HMAC is byte-exact)
- read @files directly so character devices like /dev/null work
misc: clearer --headers comma error; delete dead renderVerificationWarning.
Rejected on judgment: routing verify --json failures to stdout (would break the
CLI-wide convention that errors are JSON on stderr; commands throw, never exit).
Deferred: global Commander-error JSON formatting (touches all commands).
Claude-Session: https://claude.ai/code/session_01SYYJBsRxBQjCAuNbQiiLma
The header sweep stepped the cursor up by the body's newline count, which under-counts any line that wraps past the terminal width. The overshoot left a duplicate "Next steps" stranded on screen. Extract `cursorRowsBelowHeader` to add the extra rows each wrapped line spans, and use the live terminal width. Claude-Session: https://claude.ai/code/session_01Nimc3t87jP8rm8DZ33kpJi
…ect spinner Collapse the listen delivery path now that V1 is relay-only: extract `assertForwardTo`, drop the dead PLAPI banner branch, and trim `ReadyInfo` to the fields the relay flow actually emits. Wrap the relay handshake in `withSpinner` (a no-op in agent/--json mode) and render the `token` Next steps through the animated `outro`. Claude-Session: https://claude.ai/code/session_01Nimc3t87jP8rm8DZ33kpJi
- relay-client: use .finally() so redial fires even if onTokenRotated rejects - verify: preserve underlying error cause instead of collapsing to FILE_NOT_FOUND - config: guard relay parsing against arrays and validate RelayEntry shape - docs: add token to webhooks subcommand list in command-registration rule
- Replace --headers (comma-separated) with repeatable -H/--header flag so duplicate Set-Cookie headers are preserved end-to-end via Headers.append() - Strip hop-by-hop headers (host, connection, transfer-encoding, etc.) before forwarding so the request looks like it originated at the forward target - Add .catch() on onTokenRotated() chain to prevent unhandled promise rejection - Use Promise.withResolvers<void>() in RelayClient.start() and webhooksListen - Add SIGINT double-press re-entrancy guard and 2s drain timeout in listen - Upgrade dropped-delivery log from debug to warn - Add IRelayClient interface so FakeRelayClient in tests can use implements - Rename cliSigintHandler to CLI_SIGINT_HANDLER (global handler naming convention) - Reword "Pin it:" to "Use this token:" for clarity
c45580c to
60ec354
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli-core/src/commands/webhooks/forward.ts`:
- Around line 19-29: The hop-by-hop header blocklist in HOP_BY_HOP_HEADERS is
using the wrong trailer entry, so update the set to exclude the real Trailer
header name instead of trailers. Make this change in the forward.ts header
filtering logic so the forwarding path correctly strips relay-supplied trailer
declarations before sending the request.
- Around line 60-68: `buildForwardHeaders()` is re-adding user-supplied
`--header` values from `extraHeaders`, which lets hop-by-hop headers slip
through despite earlier filtering. Update the header merge loop in `forward()`
to skip the same hop-by-hop names that are stripped from `eventHeaders` (for
example `host`, `connection`, and related connection-specific fields), not just
`svix-*`. Make sure the filtering applies before calling `headers.set` or
`headers.append`, so `listen.ts` cannot bypass the protection through
`extraHeaders`.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ddbccf52-ae5c-42fa-9957-503e95cd1db6
📒 Files selected for processing (10)
packages/cli-core/src/cli.tspackages/cli-core/src/commands/webhooks/forward.test.tspackages/cli-core/src/commands/webhooks/forward.tspackages/cli-core/src/commands/webhooks/index.tspackages/cli-core/src/commands/webhooks/listen.test.tspackages/cli-core/src/commands/webhooks/listen.tspackages/cli-core/src/commands/webhooks/relay-client.tspackages/cli-core/src/commands/webhooks/token.test.tspackages/cli-core/src/commands/webhooks/token.tspackages/cli-core/src/lib/signals.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- packages/cli-core/src/commands/webhooks/index.ts
- packages/cli-core/src/commands/webhooks/token.test.ts
- packages/cli-core/src/lib/signals.ts
- packages/cli-core/src/cli.ts
- packages/cli-core/src/commands/webhooks/token.ts
- packages/cli-core/src/commands/webhooks/relay-client.ts
- packages/cli-core/src/commands/webhooks/listen.ts
- packages/cli-core/src/commands/webhooks/listen.test.ts
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli-core/src/commands/webhooks/listen.test.ts`:
- Around line 69-80: The shared captured log buffers are being reused across
startListen() calls, so stale ready output can cause later tests to skip waiting
and race webhooksListen() before it is actually ready. Clear captured.out and
captured.err at the start of startListen() before invoking webhooksListen(), so
each test run waits on fresh output. Keep the readiness check tied to the
existing startListen helper and its webhooksListen call site, and apply the same
reset behavior to any other tests that reuse the same captured object.
In `@packages/cli-core/src/commands/webhooks/README.md`:
- Around line 65-68: The ready-line example in the webhooks README uses invalid
JSON, so update the `--json` schema example to a valid object with properly
quoted keys and values that matches the `ready` payload shape used by the
webhook output docs. Keep the surrounding description intact, and ensure the
example under the ready-line schema clearly shows `type`, `relay_url`, and
`forward_to` in valid JSON syntax.
- Around line 43-48: The webhooks README is documenting the wrong header flag
shape: it currently shows a plural, comma-separated `--headers <pairs>` option,
but the command surface and tests use repeatable `--header` entries. Update the
option table and surrounding text in the webhooks README to reflect the actual
`--header` flag, describe it as a single header pair per use, and keep the note
about `svix-*` headers not being overridable.
In `@packages/cli-core/src/commands/webhooks/relay-client.ts`:
- Around line 54-72: The start() flow in relay-client.ts is resolving too early
in RelayClient.start()/resolveFirstOpen, before the relay token is actually
accepted. Update the first-connect handshake so callers only see “ready” after a
positive acknowledgment, or otherwise delay/refetch readiness when the initial
websocket is closed with a token-rotation/1008 path. Make sure the logic around
connect(), resolveFirstOpen, and the close handling keeps the printed relay URL
from becoming stale when the token is rejected and renewed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6b707600-e363-4250-8850-edc9c8c603fa
📒 Files selected for processing (29)
.changeset/webhooks-listen-v1.md.claude/rules/command-registration.md.oxlintrc.jsonpackages/cli-core/src/cli-program.tspackages/cli-core/src/cli.tspackages/cli-core/src/commands/webhooks/README.mdpackages/cli-core/src/commands/webhooks/forward.test.tspackages/cli-core/src/commands/webhooks/forward.tspackages/cli-core/src/commands/webhooks/index.tspackages/cli-core/src/commands/webhooks/listen.test.tspackages/cli-core/src/commands/webhooks/listen.tspackages/cli-core/src/commands/webhooks/relay-client.test.tspackages/cli-core/src/commands/webhooks/relay-client.tspackages/cli-core/src/commands/webhooks/relay-protocol.test.tspackages/cli-core/src/commands/webhooks/relay-protocol.tspackages/cli-core/src/commands/webhooks/render.test.tspackages/cli-core/src/commands/webhooks/render.tspackages/cli-core/src/commands/webhooks/shared.tspackages/cli-core/src/commands/webhooks/token.test.tspackages/cli-core/src/commands/webhooks/token.tspackages/cli-core/src/commands/webhooks/verify.test.tspackages/cli-core/src/commands/webhooks/verify.tspackages/cli-core/src/lib/config.tspackages/cli-core/src/lib/errors.tspackages/cli-core/src/lib/gradient.test.tspackages/cli-core/src/lib/gradient.tspackages/cli-core/src/lib/input-json.test.tspackages/cli-core/src/lib/input-json.tspackages/cli-core/src/lib/signals.ts
✅ Files skipped from review due to trivial changes (3)
- .claude/rules/command-registration.md
- .oxlintrc.json
- .changeset/webhooks-listen-v1.md
🚧 Files skipped from review as they are similar to previous changes (22)
- packages/cli-core/src/commands/webhooks/shared.ts
- packages/cli-core/src/lib/input-json.test.ts
- packages/cli-core/src/lib/errors.ts
- packages/cli-core/src/lib/input-json.ts
- packages/cli-core/src/lib/signals.ts
- packages/cli-core/src/commands/webhooks/token.ts
- packages/cli-core/src/commands/webhooks/token.test.ts
- packages/cli-core/src/cli.ts
- packages/cli-core/src/lib/gradient.ts
- packages/cli-core/src/commands/webhooks/render.test.ts
- packages/cli-core/src/commands/webhooks/render.ts
- packages/cli-core/src/cli-program.ts
- packages/cli-core/src/lib/gradient.test.ts
- packages/cli-core/src/commands/webhooks/relay-client.test.ts
- packages/cli-core/src/lib/config.ts
- packages/cli-core/src/commands/webhooks/relay-protocol.test.ts
- packages/cli-core/src/commands/webhooks/index.ts
- packages/cli-core/src/commands/webhooks/forward.ts
- packages/cli-core/src/commands/webhooks/verify.ts
- packages/cli-core/src/commands/webhooks/verify.test.ts
- packages/cli-core/src/commands/webhooks/listen.ts
- packages/cli-core/src/commands/webhooks/relay-protocol.ts
Thread an optional `examples` list through CliError / throwUsageError and
add `formatExamplesBlock` so usage errors can print "the right way" to run a
command: a `$ `-prefixed, column-aligned block in human mode, and raw
{command, description} pairs in the JSON error for agents to retry against.
Wire it into `webhooks listen`: a missing `--forward-to` now shows a
`<url>`-placeholder example. Also polish the `token` Next steps and the
`listen` ready banner so the Relay URL is explicit.
|
!snapshot |
Snapshot publishednpm install -g clerk@2.0.1-snapshot.daacb49
|
Summary
Adds the
clerk webhookscommand group (V1) — a PLAPI-free local webhooks toolkit. Every command runs with no auth, no linked project, and no Clerk backend, so a developer can test webhook delivery and signature verification before any dashboard setup.Three subcommands:
clerk webhooks listen --forward-to <url>— opens a standalone Svix relay tunnel and forwards each delivery to a local handler.--forward-tois required. Without--tokenthe banner warns that the auto-generated relay token isn't a guaranteed-stable handle and prints the exact--tokento pin next time;--token <c_…>pins an explicit, shareable URL that survives restarts (persisted in config). Flags:--forward-to(required),--token,--headers,--json.clerk webhooks verify— verifies a webhook signature offline (HMAC-SHA256), from a savedlistenevent line (--delivery) or the four explicit header values. No network calls.clerk webhooks token— generates a valid relay token (c_+ 10 base62 chars) forlisten --token. Prints the bare token to stdout so it pipes:clerk webhooks listen --token "$(clerk webhooks token)".Agent mode
listenemits NDJSON (ready/event/reconnectinglines) under--jsonor in agent mode;verifyandtokenemit machine-readable output too. Usage/validation errors surface as JSON in agent mode (e.g. a malformed--tokenor missing--forward-to).