Gareth Wilson Gareth Wilson

How to Connect External Webhooks to Claude Code Using Channels and Hookdeck CLI

Published


Anthropic just shipped Claude Code Channels — a way to push external events directly into a running Claude Code session. Think CI failures, monitoring alerts, payment webhooks, GitHub notifications. Instead of Claude waiting for you to ask it something, it can now react to things happening in your systems while you're not even at the terminal.

Currently available as a research preview, the webhook use case is one of the most immediately practical applications for Channels. The problem is that webhook providers need a public URL to send events to, and your Claude Code session is running locally. This is the same problem every developer faces when testing webhooks in development — and it's exactly what Hookdeck CLI was built to solve.

This walkthrough shows you how to wire everything together: build a simple webhook channel server, use Hookdeck CLI to give it a public URL, and get Claude Code reacting to real webhook events from GitHub as an example (you can switch providers to whatever you like).

What you'll need

Before you start, make sure you have:

  • Claude Code v2.1.80 or later — channels are new and require a recent version. Run claude --version to check.
  • A claude.ai login — channels don't work with Console or API key authentication.
  • Bun installed — the official channel examples use Bun for its built-in HTTP server and TypeScript support. Install it from bun.sh if you don't have it.
  • Hookdeck CLI installed — run npm install hookdeck-cli -g or brew install hookdeck/hookdeck/hookdeck. No account required.
  • A GitHub repo you can add a webhook to (for the demo). Any repo works, including a test one.

If you're on a Claude Team or Enterprise plan, your admin needs to enable channels first. Individual and Pro/Max Claude users should be fine.

How Channels work (the 30-second version)

A channel is an MCP server that Claude Code spawns as a subprocess. It communicates with Claude Code over stdio — the same way other MCP servers work. What makes it a channel specifically is that it declares a claude/channel capability and emits notification events that Claude Code listens for.

The flow for webhooks looks like this:

  1. An external service (GitHub, Stripe, your CI pipeline) sends a webhook
  2. Your channel server receives it on a local HTTP port
  3. The server pushes it into the Claude Code session as a notification
  4. Claude reads the notification and acts on it — reading files, running commands, fixing code, whatever the situation calls for

The gap in this flow is step 2: that external service needs a public URL, but your channel server is running on localhost. That's where Hookdeck CLI comes in.

Step 1: Build the webhook channel server

Create a new directory for your channel and set up the project:

mkdir webhook-channel && cd webhook-channel
bun init -y
bun add @modelcontextprotocol/sdk

Now create a single file called webhook.ts. This is the entire channel server — an MCP server that also runs an HTTP listener:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// Configure the port — Hookdeck CLI will forward webhooks here
const PORT = parseInt(process.env.WEBHOOK_PORT || "8788");

// Create the MCP server with the channel capability
const mcp = new McpServer(
  { name: "webhook", version: "1.0.0" },
  { capabilities: { experimental: { "claude/channel": {} } } }
);

// Start the HTTP listener for incoming webhooks
const server = Bun.serve({
  port: PORT,
  async fetch(req) {
    if (req.method !== "POST") {
      return new Response("Send a POST request", { status: 405 });
    }

    const body = await req.text();
    const url = new URL(req.url);

    // Build metadata from headers that webhook providers commonly send
    const meta: Record<string, string> = {
      source: "webhook",
      path: url.pathname,
      method: req.method,
    };

    // Capture x- headers as metadata (covers GitHub, Stripe, Hookdeck, etc.)
    for (const [key, value] of req.headers) {
      if (key.startsWith("x-")) {
        meta[key.replace(/^x-/, "").replace(/-/g, "_")] = value;
      }
    }

    // Send the notification to Claude Code
    await mcp.notification({
      method: "notifications/claude/channel",
      params: {
        content: meta.github_event
          ? `GitHub ${meta.github_event}: ${body}`
          : body,
        meta: meta,
      },
    });

    console.error(
      `[webhook] Forwarded ${meta.github_event || "event"} to Claude`
    );
    return new Response("OK", { status: 200 });
  },
});

console.error(`[webhook] HTTP listener on port ${PORT}`);

// Connect to Claude Code via stdio
const transport = new StdioServerTransport();
await mcp.connect(transport);

A few things to notice here. All logging uses console.error, not console.log — that's important because stdout is reserved for MCP communication between the server and Claude Code. The notification uses content for the payload Claude will read and meta for structured metadata (headers, source info) that Claude can reference when deciding how to act.

Step 2: Register the server with Claude Code

Add the channel server to your .mcp.json file. You can put this in the project directory (so it applies to that project) or in ~/.mcp.json (so it's available everywhere):

{
  "mcpServers": {
    "webhook": {
      "command": "bun",
      "args": ["run", "./webhook-channel/webhook.ts"],
      "env": {
        "WEBHOOK_PORT": "8788"
      }
    }
  }
}

Adjust the path in args to wherever you saved webhook.ts. If you're putting this in a project-level .mcp.json, a relative path works. For ~/.mcp.json, use the full absolute path.

Next, create a .claude/channels.json file in the same location (project directory or home directory) to register the channel:

{
  "webhook": {
    "server": "webhook"
  }
}

This tells Claude Code that the webhook MCP server should be treated as a channel — linking the channel name to the server defined in .mcp.json.

Step 3: Start Hookdeck CLI

The easiest way to get set up is through the Hookdeck Console. When you open it, Hookdeck auto-creates a source with a public webhook URL and gives you the exact CLI commands to run. Set the destination to CLI, and you'll see instructions like this:

hookdeck login --cli-key YOUR_CLI_KEY
hookdeck listen 8788 YOUR_SOURCE_NAME

Run both commands in a separate terminal window. The first authenticates the CLI, and the second starts forwarding webhooks from your source to localhost:8788.

Once it's running, you'll see output like this:

Dashboard
👉 Inspect and replay webhooks: https://dashboard.hookdeck.com/cli/events

YOUR_SOURCE_NAME Source
🔌 Webhook URL: https://hkdk.events/src_xxxxxxxxxx

Connections
cli forwarding to /

> Ready! (^C to quit)

Copy that webhook URL — it's the public URL you'll give to GitHub (or any webhook provider). This URL is stable; it won't change between CLI sessions, so you only need to configure your webhook provider once.

Keep this terminal open. The CLI needs to stay running to forward webhooks to your local server.

Step 4: Start Claude Code with the channel enabled

Since channels are in research preview, custom channels aren't on the approved allowlist yet. You need to start Claude Code with a flag that bypasses the check:

claude --dangerously-load-development-channels server:webhook

The server:webhook part refers to the server name in your .mcp.json. When Claude Code starts, it reads .mcp.json, spawns your webhook.ts as a subprocess, and the HTTP listener starts automatically on port 8788.

You should see the server start up in Claude Code's output. If you see "blocked by org policy," your Team or Enterprise admin needs to enable channels via the channelsEnabled managed setting.

Step 5: Test with a mock webhook

Before wiring up GitHub, it's worth verifying that the full chain works — CLI to local server to Claude Code. Hookdeck Console has a built-in library of mock webhooks from real providers, including GitHub, so you can send a realistic test event without configuring anything externally.

Open the Hookdeck Console — select the 'Webhooks Library' and find the mock event you'd like to send from the list of Webhook producers, in our example it's GitHub. Click Send.

You should see the mock event flow through in real time: it appears in the Hookdeck CLI terminal, gets forwarded to localhost:8788, and arrives in your Claude Code session as a channel notification. If Claude reacts to it (reading the payload, acknowledging the event), everything is connected correctly.

This is also a good moment to iterate on your channel server's notification formatting. If the payload doesn't look right in Claude Code, tweak webhook.ts, restart Claude Code, and replay the same mock event from the Console. No need to re-trigger anything from an external provider.

Once you've confirmed the chain works end to end, you're ready to connect a real webhook provider.

Step 6: Configure GitHub to send webhooks

Go to your GitHub repo, then Settings → Webhooks → Add webhook.

  • Payload URL: paste the Hookdeck webhook URL from step 3 (the https://hkdk.events/src_xxxxxxxxxx one)
  • Content type: select application/json
  • Events: select "Let me select individual events" and check Push events (or whatever you want Claude to react to)
  • Active: make sure it's checked

Click "Add webhook." GitHub will send a ping event immediately, which you should see arrive in both the Hookdeck CLI terminal and your Claude Code session.

Step 7: Trigger a webhook and watch Claude react

Push a commit to your repo. Something small works fine — edit a README, add a comment to a file, anything.

What happens next:

  1. GitHub sends a push webhook to the Hookdeck URL
  2. Hookdeck captures the event (you can see it in the CLI output and the Hookdeck dashboard)
  3. Hookdeck forwards it to localhost:8788
  4. Your channel server receives it, sends a channel notification with the payload and metadata to Claude Code
  5. Claude Code receives the event and acts on it

In your Claude Code terminal, you'll see the webhook arrive as a channel event. Claude reads the push payload — which includes the commit message, changed files, author, and branch — and can start responding. Depending on what you've told Claude to do (or what your CLAUDE.md instructions say), it might review the changes, run tests, check for issues, or just acknowledge the event.

Why Hookdeck CLI and not just ngrok?

You could use ngrok or any other tunnel here — the channel server doesn't care how webhooks reach its local port. But Hookdeck CLI has a few advantages that matter specifically for channel development:

Stable URLs. Your Hookdeck webhook URL stays the same across sessions. With ngrok (on the free tier), you get a new URL every time you restart, which means re-configuring your webhook provider each time. When you're iterating on a channel and restarting Claude Code frequently, this is a real timesaver.

Event replay. This is the killer feature for channel development. Once Hookdeck captures a webhook, you can replay it instantly — press r in the CLI to re-send the selected event to your local server. This means you can iterate on your channel server's notification formatting, restart Claude Code, and replay the same webhook without having to push another commit or re-trigger the event from the source. The feedback loop goes from minutes to seconds.

Event inspection. The CLI shows the full request and response for each webhook, and the Hookdeck dashboard gives you a web UI with filtering and search across your event history. When you're debugging why Claude isn't reacting to an event the way you expect, being able to inspect the exact payload and headers that arrived is more useful than adding more console.error statements.

Filtering. If you're subscribed to a lot of GitHub events but only want your channel to receive push events, you can set up filters in Hookdeck to drop everything else before it reaches your local server. Less noise for Claude to deal with.

Customising what Claude does with the events

Out of the box, Claude will read the webhook payload and decide what to do based on context. You can guide this by adding instructions to your project's CLAUDE.md file:

## Webhook handling

When you receive a GitHub push webhook via the webhook channel:
1. Check which files were changed in the commit
2. If any test files were modified, run the test suite
3. If the commit message mentions "fix" or "bug", review the changed files for potential issues
4. Summarise what was pushed in a short message

This gives Claude a playbook for handling specific event types. You could also differentiate by event type using the meta object in the notification — the github_event field tells Claude whether it's a push, pull request, issue, or something else.

Extending the channel server

The webhook.ts file is deliberately simple. Here are a few ways to extend it for more specific use cases:

Add sender gating. Right now, the server accepts any POST request. For development this is fine (Hookdeck handles authentication on its end), but for anything more exposed you should validate the source. You could check the x-hookdeck-signature header, or restrict to localhost-only connections since Hookdeck CLI forwards from the same machine.

Parse well-known formats. Instead of forwarding raw JSON, you could parse GitHub or Stripe payloads into a more readable summary. Claude will parse JSON fine, but a human-readable summary in the notification title makes it easier to see what's happening at a glance.

Make it two-way. The channels reference docs show how to expose a reply tool so Claude can send messages back. For webhooks, this could mean posting a comment on a GitHub PR, acknowledging an alert in a monitoring tool, or sending a response payload to a downstream service.

What about production?

Channels are in research preview, so this is all development-focused for now. A few things to keep in mind:

  • Events only arrive while the Claude Code session is open. For always-on setups, you'd run Claude Code in a persistent terminal or background process.
  • The --dangerously-load-development-channels flag is for testing. Custom channels will eventually go through an approval process or plugin marketplace.
  • If Claude hits a permission prompt while you're away from the terminal, the session pauses until you approve. For unattended use, --dangerously-skip-permissions bypasses this, but only use it in environments you trust.

For a production-grade setup, you'd likely use Hookdeck's full Event Gateway (there's a generous free plan) with proper delivery guarantees, retries, and rate limiting feeding into a channel server that runs alongside a persistent Claude Code session.

What's next: a Hookdeck channel plugin

The walkthrough above has you building a channel server by hand, which is useful for understanding how the pieces fit together. But there's a more integrated version possible: a Hookdeck plugin that handles all of this automatically.

Imagine running claude --channels hookdeck and having it provision Hookdeck sources, set up the local forwarding, and start pushing events to Claude — no manual server code, no separate CLI session. Because channels are MCP servers and the Hookdeck SDK provides programmatic access to the Event Gateway API, a plugin would auto-configure connections on startup and expose Hookdeck's event replay and filtering directly to Claude.

We're working on getting this approved. In the meantime, Hookdeck already has webhook-skills covering provider-specific webhook handling for Stripe, GitHub, Shopify, and others, agent-skills with Event Gateway knowledge baked in, and a MCP server baked into the CLI for agents to use.