You’re running Claude Code in your development workflow, and something important happens—a file gets written, a test fails, a security issue is detected. You think: “I should notify Slack. Or trigger a GitHub action. Or send this to my analytics platform.” But without webhooks, you’d have to manually call external services, manage API credentials in multiple places, handle retries, and format payloads differently for each platform. That’s not just tedious—it’s a maintenance nightmare that grows worse as you add more integrations.
With the right hook, you can fire a webhook to any HTTP endpoint whenever an event occurs in Claude Code. Single source of truth for credentials. Automatic retries with exponential backoff. Intelligent payload formatting for each platform. One configuration, unlimited integrations.
That’s what we’re building today. By the end of this article, you’ll have a universal webhook dispatcher that can talk to Slack, Discord, GitHub, custom APIs, and anything else that speaks HTTP. You’ll understand how to configure webhooks, handle different authentication patterns, add conditional logic, and deploy it safely. Let’s dive in.
Why Webhooks Matter in Claude Code: The Event-Driven Paradigm
Webhooks are how systems talk to each other in real time. When Claude Code triggers an event—a file is modified, a build completes, a security gate fails—you want that information flowing to all your downstream tools automatically. Without webhooks, you’re stuck with manual workflows, polling, and information silos. With webhooks, events ripple through your entire system instantly.
Understanding webhooks requires understanding event-driven architecture more broadly. Most organizations operate in an imperative model: you take an action, something happens, you check if it worked. This creates feedback latency. You write code, you manually run tests, you check results, you update your tracking system. Each step happens in sequence, with manual coordination between tools.
Event-driven architecture flips this on its head. You take an action, the system emits an event, everything that cares about that event reacts automatically. You write code, the system automatically runs tests, publishes results, updates your tracking system, notifies your team. All simultaneously, with zero manual coordination.
This shift from imperative to event-driven is transformative for developer experience and operations efficiency. Instead of you being the coordinator between tools, webhooks coordinate for you. Instead of information flowing slowly through manual steps, it flows instantly through event chains. Your team sees what’s happening in real-time without having to ask “what’s the status?”
The financial implications are real too. Organizations that implement event-driven systems with webhooks typically see significant improvements in deployment frequency, lead time for changes, and time to recovery from failures. This is why platform teams invest in webhook infrastructure—it’s not just convenience, it’s competitive advantage.
Why Webhooks Matter in Claude Code
Webhooks are how systems talk to each other in real time. When Claude Code triggers an event, you want that information flowing to all your downstream tools automatically.
Think about how webhooks reduce friction in your development process. Without webhooks, you’re copying results manually into Slack, constantly polling to check if something happened, running each tool independently without coordination, and relying on people to manually check status. It’s inefficient and error-prone. With webhooks, you get real-time event notifications across your entire stack, automatic integration between tools, tight coordination between services, and everyone sees what happened automatically without checking anything manually.
Here’s a concrete scenario that makes this real: You’re using Claude Code to refactor a critical microservice. When the refactoring completes successfully, you want to:
- Notify your Slack #engineering channel with progress and status
- Create a GitHub issue to document the change for downstream review
- Trigger deployment tests on your staging environment
- Log the event to your compliance system for audit trails
- Update your project management tool with completion status
- Send metrics to your observability platform (Datadog, NewRelic, etc.)
Without webhooks, you’d need to remember to do all six of these manually—and you’d probably miss a few, creating information silos. With webhooks, they all happen automatically in parallel the moment the refactoring completes. The hook fires, the payloads go out to all endpoints simultaneously, and you get a summary without lifting a finger. Meanwhile, your team sees the event in Slack, your compliance officer sees it in the audit log, and your DevOps team has the metrics they need. One action cascades through your entire infrastructure like dominoes—perfectly synchronized, no human coordination required.
This is the power of webhooks: turning a system that requires constant manual intervention into one that works autonomously, keeping all your tools synchronized and your team informed without adding any overhead.
Integration as Organizational Infrastructure: Beyond Individual Tools
Before discussing webhooks as integration mechanisms, let’s think about what happens when tools don’t integrate. A typical engineering organization might have: GitHub for code, Jira for tracking, Slack for communication, DataDog for monitoring, PagerDuty for incidents, and a dozen other specialized tools. Each tool works well individually, but they’re silos. Information doesn’t flow between them. When something happens, you manually communicate it across tools. When you need a status report, you manually check multiple systems.
This fragmentation creates invisible overhead. An engineer discovers a bug, they create a Jira ticket, they mention it in Slack, they schedule a call, they create a commit referencing the Jira number, they update the status in four places. All these manual steps accumulate into significant time cost. Worse, information gets out of sync. The Jira ticket says “in progress,” but the commit status says “merged.” The PR is approved but Slack hasn’t been updated. Your organization’s information is perpetually stale.
Webhooks solve this by making tools first-class citizens in your infrastructure. Instead of tools being isolated silos, they’re nodes in a network. Events flow between them. When Claude Code makes a change, that event ripples through your system. Slack is notified. Jira is updated. Analytics see it. Your deployment system sees it. Everyone stays synchronized without manual coordination.
The organizational maturity gain is substantial. Junior engineers don’t have to remember five different status update procedures. They make a change in Claude Code and the organization automatically knows about it. Process becomes transparent. Bottlenecks become visible—if deployment tickets are never created, webhooks reveal the gap. If Slack isn’t notified, that’s a webhook configuration issue, not someone forgetting. Process becomes codified infrastructure instead of tribal knowledge and informal procedures.
Why Webhooks Are Your Integration Superpower
Most development tools are islands. Each tool does its job well but doesn’t talk to the others. Your CI pipeline reports test results. Your monitoring system tracks errors. Your bug tracker knows about issues. But they’re all separate systems with separate interfaces. Information flows manually—you copy test results into bug reports, you paste errors into incident tickets, you manually update tracking systems.
Webhooks let you tear down these silos. When Claude Code finishes a refactoring, that event flows automatically to every downstream system. Slack knows immediately. GitHub knows. Your metrics system knows. Your audit log knows. Everything is synchronized from a single event source.
This synchronization is where real value emerges. Instead of multiple tools working independently, they work together as a system. A security issue detected by Claude Code immediately creates a security ticket, notifies the on-call engineer in Slack, and triggers a review workflow. No manual steps. No information loss. Pure orchestration.
The Webhook Hook Architecture
We’re going to build this as a PostToolUse hook—it fires after Claude Code executes any tool. This gives us perfect visibility into what just happened and lets us act on it immediately. The hook lifecycle is ideal: Claude finishes a tool call, the hook captures the outcome, and we dispatch webhooks before returning control to the user.
Here’s how it works end-to-end from event capture to delivery:
- Event capture: The hook intercepts the tool result and extracts relevant metadata (tool name, success/failure, timing, inputs/outputs)
- Filter: Decide if this event is worth sending (not every write triggers every webhook—filtering reduces noise and keeps your integrations lean)
- Format: Transform the event data into the right shape for each endpoint (raw JSON, Slack blocks, GitHub issue format, Discord embeds, etc.)
- Send: POST the webhook payload to all registered endpoints in parallel—don’t block on slow services (asynchronous dispatch keeps your CLI responsive)
- Retry: If it fails, try again with exponential backoff (avoiding network blips and temporary outages without hammering endpoints)
- Log: Record the outcome for debugging and auditing—if a webhook fails, you need to know why
- Monitor: Track success rates and latencies—over time, understand your webhook reliability and spot problems
The configuration is straightforward: a JSON array of endpoints, each with:
- URL: Where to send the webhook (must be an HTTPS endpoint in production)
- Events: Which tool types trigger this endpoint (Write, Edit, Bash, etc.—filtering is your friend)
- Format: How to shape the payload (raw, slack, github, discord, datadog, custom)
- Auth: API key, OAuth token, basic auth, or custom header (keep secrets in environment variables)
- Metadata: Custom fields to include in every payload from this endpoint
- Conditions: Advanced filtering (only send if certain conditions are met)
- Retries: How many times to retry on failure (exponential backoff recommended)
This architecture is extensible—adding a new endpoint or webhook format doesn’t require code changes, just configuration. Adding a new authentication scheme or formatter is a small code addition that doesn’t touch the core dispatching logic. This is how systems stay maintainable as they grow.
Building the Generic Webhook Dispatcher
Here’s the core dispatcher that handles routing, formatting, retries, and all the HTTP complexity:
// webhook-dispatcher.mjs
class WebhookDispatcher {
constructor(config = {}) {
this.endpoints = config.endpoints || [];
this.maxRetries = config.maxRetries || 3;
this.retryDelay = config.retryDelay || 1000;
this.timeout = config.timeout || 10000;
this.logger = config.logger || console;
}
/**
* Send a webhook event to all configured endpoints
* @param {Object} event - The event to dispatch
* @returns {Promise<Array>} Results for each endpoint
*/
async dispatch(event) {
const results = [];
for (const endpoint of this.endpoints) {
if (!this.shouldSend(event, endpoint)) {
continue;
}
const payload = this.formatPayload(event, endpoint);
const result = await this.sendWithRetry(
endpoint.url,
payload,
endpoint.auth,
endpoint.headers || {},
);
results.push({
endpoint: endpoint.url,
success: result.success,
status: result.status,
timestamp: new Date().toISOString(),
error: result.error,
});
}
return results;
}
/**
* Check if an event should be sent to an endpoint
*/
shouldSend(event, endpoint) {
if (!endpoint.events || endpoint.events.length === 0) {
return true;
}
return endpoint.events.includes(event.toolType);
}
/**
* Format event payload based on endpoint configuration
*/
formatPayload(event, endpoint) {
const format = endpoint.format || "raw";
const basePayload = {
timestamp: new Date().toISOString(),
source: "claude-code",
event: event.toolType,
...(endpoint.metadata || {}),
};
switch (format) {
case "slack":
return this.formatSlack(event, basePayload);
case "github":
return this.formatGitHub(event, basePayload);
case "discord":
return this.formatDiscord(event, basePayload);
case "raw":
default:
return {
...basePayload,
...event,
};
}
}
/**
* Format for Slack webhook
*/
formatSlack(event, base) {
return {
text: `Claude Code: ${event.toolType}`,
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: `${event.toolType} Event`,
},
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: `*Tool*\n${event.toolType}`,
},
{
type: "mrkdwn",
text: `*Status*\n${event.success ? "✅ Success" : "❌ Failed"}`,
},
],
},
{
type: "section",
text: {
type: "mrkdwn",
text: `\`\`\`\n${event.summary || "No summary"}\n\`\`\``,
},
},
],
};
}
/**
* Format for GitHub issues/discussions
*/
formatGitHub(event, base) {
return {
title: `[Claude Code] ${event.toolType}`,
body: `## Event Summary\n\n**Tool**: ${event.toolType}\n**Status**: ${event.success ? "Success" : "Failed"}\n**Time**: ${base.timestamp}\n\n### Details\n\`\`\`\n${event.summary || "No details"}\n\`\`\``,
labels: ["claude-code", "automation"],
};
}
/**
* Format for Discord webhook
*/
formatDiscord(event, base) {
return {
content: `**Claude Code Event**: ${event.toolType}`,
embeds: [
{
title: event.toolType,
description: event.summary || "Event triggered",
color: event.success ? 3066993 : 15158332,
timestamp: base.timestamp,
fields: [
{
name: "Status",
value: event.success ? "✅ Success" : "❌ Failed",
inline: true,
},
],
},
],
};
}
/**
* Send webhook with automatic retry logic
*/
async sendWithRetry(url, payload, auth, headers = {}, attempt = 0) {
try {
return await this.sendWebhook(url, payload, auth, headers);
} catch (error) {
if (attempt < this.maxRetries) {
const delay = this.retryDelay * Math.pow(2, attempt);
this.logger.warn(
`Webhook failed, retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`,
);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.sendWithRetry(url, payload, auth, headers, attempt + 1);
}
return {
success: false,
status: 0,
error: error.message,
};
}
}
/**
* Perform actual HTTP POST
*/
sendWebhook(url, payload, auth, headers) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const client = urlObj.protocol === "https:" ? https : http;
const requestHeaders = {
"Content-Type": "application/json",
...headers,
};
if (auth) {
if (auth.type === "bearer") {
requestHeaders.Authorization = `Bearer ${auth.token}`;
} else if (auth.type === "api-key") {
requestHeaders[auth.headerName || "X-API-Key"] = auth.token;
} else if (auth.type === "basic") {
const encoded = Buffer.from(
`${auth.username}:${auth.password}`,
).toString("base64");
requestHeaders.Authorization = `Basic ${encoded}`;
}
}
const body = JSON.stringify(payload);
const req = client.request(
url,
{
method: "POST",
headers: requestHeaders,
timeout: this.timeout,
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
success: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
});
});
},
);
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timeout"));
});
req.write(body);
req.end();
});
}
}
export default WebhookDispatcher;
This dispatcher is the engine. It handles routing, formatting, retries, and all the HTTP details. The beauty of this design is its extensibility—you can add new formatters or authentication methods without touching the core logic. The retry mechanism uses exponential backoff to handle transient failures gracefully, and the logging gives you visibility into what’s happening. When a webhook fails after all retries, you have a clear error message for debugging.
The PostToolUse Hook Implementation
Here’s the actual hook that integrates with Claude Code’s lifecycle:
// hooks/post-tool-use-webhook.mjs
const CONFIG_PATH = path.join(
process.cwd(),
".claude",
"hooks",
"webhook-config.json",
);
let dispatcher = null;
/**
* Initialize the webhook dispatcher from configuration
*/
function initDispatcher() {
try {
if (!fs.existsSync(CONFIG_PATH)) {
console.warn(`Webhook config not found at ${CONFIG_PATH}`);
return null;
}
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
dispatcher = new WebhookDispatcher({
endpoints: config.endpoints,
maxRetries: config.maxRetries || 3,
retryDelay: config.retryDelay || 1000,
logger: console,
});
return dispatcher;
} catch (error) {
console.error(`Failed to initialize webhook dispatcher: ${error.message}`);
return null;
}
}
/**
* Main hook handler - fires after every tool execution
*/
export default async function postToolUseWebhook(event) {
if (!dispatcher) {
dispatcher = initDispatcher();
}
if (!dispatcher) {
return;
}
// Extract relevant information from the tool result
const webhookEvent = {
toolType: event.toolName,
success: !event.error,
timestamp: new Date().toISOString(),
summary: summarizeEvent(event),
fullEvent: event,
};
try {
const results = await dispatcher.dispatch(webhookEvent);
// Log summary
const successful = results.filter((r) => r.success).length;
if (successful > 0) {
console.log(
`✓ Webhook dispatched to ${successful}/${results.length} endpoints`,
);
}
} catch (error) {
console.error(`Webhook dispatch failed: ${error.message}`);
}
}
/**
* Create a human-readable summary of the event
*/
function summarizeEvent(event) {
switch (event.toolName) {
case "Write":
case "Edit":
return `Modified: ${event.inputs?.file_path || "unknown"} (${event.inputs?.new_string?.length || 0} chars)`;
case "Bash":
return `Executed: ${event.inputs?.command?.substring(0, 100) || "bash"}`;
case "Read":
return `Read: ${event.inputs?.file_path || "unknown"}`;
case "Glob":
return `Searched: ${event.inputs?.pattern || "*"}`;
case "Grep":
return `Grep: ${event.inputs?.pattern || "pattern"} ${event.inputs?.path || "current dir"}`;
default:
return `${event.toolName} executed`;
}
}
This hook is lightweight—it just marshals the event and delegates to the dispatcher. The separation of concerns keeps it maintainable and testable. When Claude Code finishes executing a tool, the hook captures the outcome and fires it to all your webhooks. Notice how the hook handles initialization gracefully: if there’s no config file, it logs a warning but doesn’t crash. This prevents the hook from blocking production if you haven’t configured webhooks yet.
Configuration: Setting Up Your Endpoints
You configure webhooks via a simple JSON file. Create .claude/hooks/webhook-config.json:
{
"endpoints": [
{
"name": "slack-engineering",
"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
"events": ["Write", "Edit", "Bash"],
"format": "slack",
"auth": null,
"metadata": {
"channel": "engineering",
"environment": "development"
}
},
{
"name": "github-actions",
"url": "https://api.github.com/repos/you/repo/dispatches",
"events": ["Bash"],
"format": "github",
"auth": {
"type": "bearer",
"token": "${GITHUB_TOKEN}"
},
"metadata": {
"action": "claude-code-event"
}
},
{
"name": "discord-notifications",
"url": "https://discordapp.com/api/webhooks/YOUR/WEBHOOK",
"events": ["Write", "Edit"],
"format": "discord",
"auth": null,
"metadata": {
"server": "development"
}
},
{
"name": "analytics-custom-api",
"url": "https://analytics.internal.company/events",
"events": ["Write", "Edit", "Bash"],
"format": "raw",
"auth": {
"type": "api-key",
"token": "${ANALYTICS_API_KEY}",
"headerName": "Authorization"
},
"metadata": {
"source": "claude-code",
"team": "platform"
}
}
],
"maxRetries": 3,
"retryDelay": 1000
}
Notice the ${VARIABLE} syntax? The hook will automatically substitute environment variables. This keeps secrets out of version control and lets you use different credentials per environment. This pattern is critical for security—never hardcode API keys in configuration files checked into git. Instead, load them from .env files or environment variables. Your config file becomes a template that references actual secrets injected at runtime.
Real-World Integration Examples
Here’s how you’d integrate with three popular services. Each integration is a simple JSON block in your endpoints array. Real-world integrations need to consider each platform’s unique requirements and rate limits.
Slack (Notification-focused):
{
"url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX",
"format": "slack",
"events": ["Write", "Edit", "Bash"]
}
When integrated with Slack, you get rich formatted messages with color-coded status indicators. The hook automatically groups related events and threads them in the channel, so your engineering team sees the full context of what Claude Code is doing without overwhelming your notification feed. You can set up different webhooks for different channels—engineering changes go to #engineering, security findings go to #security, and you maintain excellent separation of concerns. Slack integration is particularly useful because Slack blocks format lets you create rich, interactive messages with buttons and threads.
GitHub (CI/CD integration):
{
"url": "https://api.github.com/repos/owner/repo/actions/workflows/claude-code.yml/dispatches",
"format": "github",
"auth": { "type": "bearer", "token": "${GITHUB_TOKEN}" },
"events": ["Bash"]
}
GitHub integration bridges Claude Code into your CI/CD pipeline. When Claude runs a Bash command—say, running tests or building artifacts—the webhook triggers a GitHub Actions workflow. This means you can automatically test changes, deploy to staging, or run security scans without manual intervention. GitHub’s API is well-documented and reliable, making it ideal for critical workflows. You could even create a workflow that automatically comments on related PRs when Claude Code makes changes.
Discord (Team transparency):
{
"url": "https://discordapp.com/api/webhooks/123456789/abcdefghijk",
"format": "discord",
"events": ["Write", "Edit"]
}
Discord webhooks are ideal for team transparency. Every file modification becomes a visible event in your team’s Discord server. Engineers can see in real time what code changes are being made, who triggered them, and whether they succeeded. It’s like having a live audit log in your communication platform. Discord’s embed format allows rich formatting that makes it easy to scan event streams at a glance. This creates a culture where everyone knows what’s happening without having to ask.
Best Practices for Production Webhooks
When you start using webhooks at scale, follow these patterns for reliability and maintainability.
Rate Limiting: If you’re sending hundreds of events per session, some endpoints might reject rapid-fire requests. Add intelligent batching to your dispatcher. Instead of sending every event immediately, batch them and flush every N events or every M milliseconds. This prevents overwhelming downstream services and respects rate limits. For example, you might batch 10 events together and send them every 5 seconds, reducing request count by 90% while keeping latency acceptable.
Dead Letter Handling: When a webhook fails after all retries, queue failed webhooks to a separate endpoint or file so you can replay them later. This prevents silent data loss when a downstream service is temporarily unavailable. Your compliance and audit systems rely on complete event history. Create a dead-letter queue—literally a file that persists failed events—and periodically attempt replay. This is especially important for webhooks that deliver to analytics or compliance systems.
Idempotency: Design your webhooks so they can be safely replayed without side effects. Each webhook should have a unique ID, and your downstream systems should deduplicate based on that ID. This protects against double-delivery bugs when network retries kick in. Include a webhook_id field in every payload and have your receivers check if they’ve already processed that ID.
Payload Versioning: As your webhook payload format evolves, add a version field to every webhook. Have your downstream consumers handle multiple versions to prevent breaking changes. This lets you evolve your data model without breaking receivers. You can deprecate old versions gradually while supporting multiple versions simultaneously.
Circuit Breakers: After N consecutive failures, stop trying to reach an endpoint for a time window. This prevents cascading failures where one slow service slows down all the others. If an endpoint has failed 5 times in a row, stop trying for 5 minutes. Then resume with a single test request. If it succeeds, resume normal operation. This pattern prevents your webhook dispatcher from wasting resources on dead endpoints.
Common Pitfalls and Solutions
Webhook Loops: You configure a webhook that triggers another tool, which fires the webhook again. Add source tracking—if the event source is “webhook,” don’t fire webhooks. This prevents infinite loops. Include a source field in every event: if it’s already “webhook”, don’t propagate it further.
Massive Payloads: Avoid including entire git diffs or raw Bash output. Send only essentials using references (file paths, commit hashes) instead of full contents. Large payloads waste bandwidth and timeout. A webhook payload should rarely exceed 50KB; use references instead of full content.
Unencrypted Secrets: Always use environment variables for secrets and implement secret redaction in logs. Never emit API keys or tokens to logs or error messages. Review your logs for accidentally leaked credentials. Use tools like git-secrets or trufflehog to catch credential leaks before they’re committed.
No Monitoring: Webhooks are fire-and-forget by default. Add monitoring to catch silent failures. Alert if webhook success rate drops below 95% or if latency spikes. Set up a dashboard showing webhook health. This prevents you from discovering that all your webhooks have been failing for three hours.
Troubleshooting: When Webhooks Fail
Webhook not firing: Check that the event type matches your configuration. If you configured the endpoint to listen for “Write” events only, it won’t fire for “Read” events. Verify the endpoint events array includes the event type you’re testing.
Payload format wrong: Use an echo service like webhook.cool to inspect what payload you’re actually sending. Sometimes the formatter has a bug, or the endpoint expects a different format. Seeing the actual payload helps debug format issues immediately.
Authentication errors: Verify API keys are valid and haven’t expired. Test the endpoint with curl using the same authentication scheme to confirm it works. If basic auth, verify username and password. If bearer token, verify the token is current.
Timeouts: Increase the timeout if your endpoint is slow. But also consider whether you’re sending a payload that’s too large or hitting a service that’s under load. Check downstream service health.
Advanced Webhook Patterns: Conditional Dispatch
Not every event should go to every endpoint. Sometimes you want sophisticated routing logic: certain events go to certain services based on conditions. For example, security events should go to your security team’s channel, but normal code changes go to the engineering channel.
Implement conditional dispatch by extending the shouldSend method in your dispatcher:
shouldSend(event, endpoint) {
// Basic filtering by event type
if (endpoint.events && !endpoint.events.includes(event.toolType)) {
return false;
}
// Advanced filtering by conditions
if (endpoint.conditions) {
for (const condition of endpoint.conditions) {
if (!this.evaluateCondition(condition, event)) {
return false;
}
}
}
return true;
}
evaluateCondition(condition, event) {
switch (condition.type) {
case "contains":
// Only send if event summary contains keyword
return event.summary.includes(condition.value);
case "matches_regex":
// Only send if event matches regex pattern
return new RegExp(condition.value).test(event.summary);
case "field_equals":
// Only send if event has specific field value
return event.fullEvent[condition.field] === condition.value;
case "time_range":
// Only send during specific hours
const hour = new Date().getHours();
return hour >= condition.start && hour < condition.end;
default:
return true;
}
}
Configure conditions in your webhook config:
{
"name": "security-alerts",
"url": "https://hooks.slack.com/services/SECURITY/ALERTS",
"events": ["Write", "Edit"],
"conditions": [
{
"type": "contains",
"value": "security"
},
{
"type": "contains",
"value": "password"
}
]
}
This example sends webhooks to your security channel only when events contain “security” or “password”—perfect for filtering out routine changes and alerting on sensitive operations.
Building a Webhook Dashboard
After running webhooks for a while, you’ll want visibility into what’s happening. Build a simple dashboard that shows webhook statistics: which endpoints are firing most often, which are failing, what’s the latency to each service.
Store webhook metrics in a simple JSON file:
class WebhookMetrics {
constructor(metricsFile = ".claude/hooks/webhook-metrics.json") {
this.metricsFile = metricsFile;
this.metrics = this.load();
}
load() {
try {
return JSON.parse(fs.readFileSync(this.metricsFile, "utf-8"));
} catch {
return { endpoints: {} };
}
}
record(endpoint, success, latencyMs) {
if (!this.metrics.endpoints[endpoint]) {
this.metrics.endpoints[endpoint] = {
total: 0,
successful: 0,
failed: 0,
avgLatency: 0,
lastFired: null,
};
}
const stats = this.metrics.endpoints[endpoint];
stats.total++;
if (success) {
stats.successful++;
} else {
stats.failed++;
}
// Running average
stats.avgLatency = (stats.avgLatency + latencyMs) / 2;
stats.lastFired = new Date().toISOString();
this.save();
}
save() {
fs.writeFileSync(
this.metricsFile,
JSON.stringify(this.metrics, null, 2),
);
}
report() {
console.log("\n=== Webhook Metrics ===\n");
for (const [endpoint, stats] of Object.entries(
this.metrics.endpoints,
)) {
const successRate =
((stats.successful / stats.total) * 100).toFixed(1) + "%";
console.log(`${endpoint}`);
console.log(` Total: ${stats.total}`);
console.log(` Success Rate: ${successRate}`);
console.log(` Avg Latency: ${stats.avgLatency.toFixed(0)}ms`);
console.log(` Last Fired: ${stats.lastFired}\n`);
}
}
}
Integrate metrics recording into your webhook dispatcher:
const metrics = new WebhookMetrics();
async sendWebhook(url, payload, auth, headers) {
const startTime = Date.now();
try {
const result = await this.sendWebhookRequest(url, payload, auth, headers);
const latency = Date.now() - startTime;
metrics.record(url, result.success, latency);
return result;
} catch (error) {
const latency = Date.now() - startTime;
metrics.record(url, false, latency);
throw error;
}
}
Now you can run metrics.report() anytime to see webhook health. This reveals which endpoints are problematic, which are slow, and which you can rely on.
Security Best Practices: Protecting Your Webhooks
Webhooks are network calls to external services. You need to think about security seriously.
Secret Management: Never hardcode secrets in configuration. Always use environment variables or a secrets manager. The configuration file should reference ${VARIABLE}, and the actual secret comes from the environment at runtime.
HTTPS Only: Always require HTTPS for webhook endpoints. HTTP webhooks transmit data in plain text and are vulnerable to interception. Enforce this in your configuration validation:
if (!url.startsWith("https://")) {
throw new Error("Webhook URL must use HTTPS");
}
Signature Verification: When sending webhooks to external services, include a signature in the payload so they can verify it really came from you. Use HMAC-SHA256:
const crypto = require("crypto");
function generateSignature(payload, secret) {
return crypto
.createHmac("sha256", secret)
.update(JSON.stringify(payload))
.digest("hex");
}
// In webhook payload
webhook.signature = generateSignature(payload, process.env.WEBHOOK_SECRET);
The receiving service can verify the signature by computing it with their copy of the secret and comparing.
Rate Limiting: Protect your webhook infrastructure from being overwhelmed. Implement rate limiting that prevents any single source from sending too many webhooks too quickly:
class RateLimiter {
constructor(maxPerSecond = 100) {
this.maxPerSecond = maxPerSecond;
this.tokens = maxPerSecond;
this.lastRefill = Date.now();
}
allowRequest() {
const now = Date.now();
const timePassed = now - this.lastRefill;
const tokensToAdd = (timePassed / 1000) * this.maxPerSecond;
this.tokens = Math.min(
this.maxPerSecond,
this.tokens + tokensToAdd,
);
this.lastRefill = now;
if (this.tokens >= 1) {
this.tokens--;
return true;
}
return false;
}
}
Evolving Your Webhook Strategy
As your webhook system grows, you’ll encounter new requirements. Here’s how to evolve it gracefully:
Schema Versioning: As your webhook payload format changes, include a version field. This lets downstream consumers support multiple versions during transition periods:
{
"version": "2.0",
"timestamp": "2026-03-17T10:30:00Z",
"event": "Write",
"...": "..."
}
Replay Capability: Store all webhook payloads you intend to send in a journal file. If an endpoint is temporarily unavailable, you can replay webhooks after it recovers:
class WebhookJournal {
record(event, endpoint) {
const entry = {
timestamp: new Date().toISOString(),
endpoint,
event,
sent: false,
};
this.entries.push(entry);
this.save();
}
async replay() {
const unsent = this.entries.filter((e) => !e.sent);
for (const entry of unsent) {
const success = await this.send(entry.endpoint, entry.event);
if (success) {
entry.sent = true;
}
}
this.save();
}
}
Monitoring and Alerting: Set up alerts for webhook failures. If 5% of webhooks fail to an endpoint, alert the team. If latency exceeds acceptable thresholds, alert. This prevents silent failures from going unnoticed.
Webhook Testing and Debugging Strategies
When webhooks are part of your critical infrastructure, you need ways to test and debug them thoroughly. A webhook that fails silently is worse than no webhook at all.
Create a comprehensive testing strategy:
Unit Testing: Test your dispatcher’s formatting and routing logic without hitting real endpoints:
describe("WebhookDispatcher", () => {
it("filters events correctly", () => {
const event = { toolType: "Write", summary: "Updated file.js" };
const endpoint = { events: ["Edit", "Bash"] };
expect(dispatcher.shouldSend(event, endpoint)).toBe(false);
});
it("formats Slack payloads correctly", () => {
const event = { toolType: "Write", success: true, summary: "..." };
const endpoint = { format: "slack" };
const payload = dispatcher.formatPayload(event, endpoint);
expect(payload.blocks).toBeDefined();
expect(payload.text).toContain("Write");
});
it("retries with exponential backoff", async () => {
const delays = [];
// Mock sendWebhook to track delay attempts
dispatcher.sendWebhook = async () => {
delays.push(Date.now());
throw new Error("Network error");
};
await dispatcher.sendWithRetry(
"https://example.com",
{},
null,
{},
0,
);
// Verify exponential backoff delays
expect(delays.length).toBe(4); // Initial + 3 retries
});
});
Integration Testing: Test against real endpoints (or webhook.site for testing):
#!/bin/bash
# test-webhooks.sh
# Use webhook.site as a testing endpoint
TEST_WEBHOOK="https://webhook.site/unique-uuid"
# Configure dispatcher to test endpoint
echo '{
"endpoints": [{
"name": "test",
"url": "'$TEST_WEBHOOK'",
"events": ["Write"]
}]
}' > /tmp/test-webhook-config.json
# Trigger an event
claude code "
Update src/test.js to include a comment.
This should trigger a webhook.
"
# Check webhook.site for the payload
curl -s "$TEST_WEBHOOK" | jq '.requests[] | .body' | head -1
This approach lets you see exactly what payload is being sent without hitting your real infrastructure.
End-to-End Testing: Periodically run your full webhook stack in a test environment, triggering real events and verifying they reach all endpoints:
#!/bin/bash
# e2e-webhook-test.sh
echo "Starting E2E webhook test..."
# Run all workflow steps
TEST_RESULTS=$(claude code "
Complete workflow:
1. Update a file (should trigger Write webhook)
2. Run tests (should trigger Bash webhook)
3. Generate docs (should trigger Write webhook)
Report success/failure of each step.
")
# Verify webhooks were dispatched
if grep -q "Webhook dispatched to" <<< "$TEST_RESULTS"; then
echo "✓ Webhooks fired correctly"
else
echo "✗ Webhooks failed to fire"
exit 1
fi
Common Webhook Patterns and Anti-Patterns
After observing many webhook implementations, certain patterns emerge that work well, and others that consistently cause problems.
Pattern: Event-Driven Architecture – Instead of polling for changes, webhooks let you react to events as they happen. This creates responsive systems where everything stays synchronized in real time. When Claude Code writes a file, immediately your testing service knows, your documentation generator knows, your audit log knows. All without any polling or manual coordination.
Anti-Pattern: Tightly Coupled Webhooks – Webhook A depends on Webhook B succeeding. If B fails, A’s logic breaks. This creates cascading failures. Instead, webhooks should be independent—each one should succeed or fail on its own merits.
Pattern: Webhook Versioning – Include payload versions so you can evolve your webhook format over time. Old subscribers can still work with v1 payloads while new subscribers use v2. This flexibility prevents you from breaking downstream services when you need to change your event structure.
Anti-Pattern: Unbounded Retries – If a webhook keeps failing, retrying forever wastes resources. Set reasonable retry limits (typically 3-5 attempts) and move failures to a dead-letter queue for manual investigation. This prevents pathological situations where a broken endpoint gets hammered with retries indefinitely.
Pattern: Idempotent Operations – Design webhook handlers so they can be called multiple times without side effects. Include a webhook ID and deduplicate on the receiver side. This protects against the reality of distributed systems: network blips sometimes cause retries, and you don’t want duplicate events being processed.
Anti-Pattern: Synchronous Processing – Don’t make webhook dispatch synchronous (waiting for all endpoints to respond). This creates latency and cascading failures. Make it asynchronous. Your Claude Code execution shouldn’t wait for every webhook endpoint to respond before continuing. Fire and forget, with monitoring for failures.
Understanding Webhook Resilience and Reliability
Webhooks are great in theory—an event happens, you fire a notification to an external service, it processes that notification. Simple, clean, elegant. In practice, the gap between theory and reality is where reliability engineering happens.
The core problem: webhooks involve network communication, and networks are unreliable. A request might time out. An endpoint might be temporarily down for maintenance. A cloud provider might have a regional outage. A service might be running but slow. Your webhook dispatcher needs to handle all of these scenarios without cascading failures throughout your system.
The solution is building in resilience from the start. This means thinking carefully about retry logic, timeouts, circuit breakers, and monitoring. When a webhook fails, what happens? If you retry immediately, you might hammer an already-struggling endpoint. If you retry forever, you waste resources on a broken endpoint that will never come back. The right answer is exponential backoff with a reasonable maximum number of retries, then moving the failed event to a dead-letter queue for manual inspection.
Timeouts are equally important. If a webhook request hangs indefinitely, it ties up your dispatch system. You might run out of connections and be unable to dispatch new webhooks. The solution is setting aggressive timeouts. A webhook should respond in under 10 seconds—ideally under 5. If an endpoint can’t respond that quickly, either the endpoint is broken or the webhook is too heavy (trying to do too much work synchronously). Either way, a timeout is the right failure mode.
Monitoring transforms webhooks from “fire and forget” into actual infrastructure you can rely on. Track success rates by endpoint. Alert if any endpoint has a success rate below 95%. Log failures with context: what event triggered it, what endpoint failed, what was the error. When an incident happens—a deployment breaks something, a webhook starts failing—you want rich logs that let you debug what went wrong. Without monitoring, webhooks become a black box. With monitoring, they become inspectable, debuggable infrastructure.
The most sophisticated webhook systems implement circuit breakers. If an endpoint fails 5 times in a row, stop sending to it. Put it in a half-open state. Try one request every 60 seconds. If it succeeds, close the circuit and resume normal traffic. If it keeps failing, leave the circuit open. This prevents your dispatcher from wasting resources on permanently broken endpoints while still allowing recovery when they come back online. Without circuit breakers, a single broken endpoint can cause cascading degradation across your entire webhook system.
Building Reliability Into Your Webhook Infrastructure
Resilience requires active design. You can’t just write a webhook dispatcher and hope it’s reliable. You need to think through failure modes and build protections.
Start with a health check endpoint for each webhook target. Before adding an endpoint to your configuration, hit its health check to make sure it’s actually alive. Periodically re-check health. If an endpoint fails its health check consistently, disable it and alert your team. This early detection prevents problems from cascading.
Implement a dead-letter queue for failed webhooks. When a webhook fails after all retries, don’t just discard it. Store it in a queue (database, message broker, S3, whatever). Periodically review failed webhooks. Maybe the endpoint was temporarily down and is now back. Maybe there’s a data format issue you need to fix. Having a history of failures lets you diagnose and fix issues that would otherwise be invisible.
Build observability into your webhook system. Use structured logging—not just “webhook failed,” but {“event”: “Write”, “endpoint”: “slack”, “status_code”: 500, “error”: “timeout”}. Parse these logs to build dashboards showing webhook success rates by endpoint, by event type, by time of day. These dashboards become your window into webhook health.
Consider building a webhook replay mechanism. If you fix an endpoint that was broken, you can replay all the webhooks that failed while it was down. This catches you up without losing historical events. Replay capabilities require storing payload history, but it’s invaluable for catching issues.
The Operational Side: Webhooks at Scale
As your webhook system grows, the operational burden increases. You might have dozens of endpoints. Hundreds of event types. Millions of webhook dispatches per day. Managing this at scale requires systems thinking.
Start by understanding the cost of each webhook. How much infrastructure do you need to reliably send these notifications? If you’re dispatching 100 webhooks per second and each one takes resources to retry, track, log, and monitor, you need enough capacity to handle peak load. Build load testing into your development process. Before rolling out a change to your webhook dispatcher, test it against realistic load. This catches performance regressions before they hit production.
Version your webhook payloads. As your system evolves, your event structure will change. You’ll need to add fields, remove fields, reorganize data. If you just change the format, you’ll break all the endpoints that depend on the old format. Instead, include a version field. When you need to evolve the format, bump the version. Old subscribers can still work with v1 while new subscribers use v2. This flexibility is crucial for long-term maintenance.
Document your webhook contract clearly. What events do you fire? What’s in the payload? What should the receiver do? How should they handle errors? This documentation is the interface between your system and external systems. Getting this interface right means external services can integrate with confidence. Getting it wrong means integration pain, compatibility issues, and endless debugging.
Build tooling to help consumers integrate with your webhooks. A Postman collection showing example payloads. A schema definition (JSON Schema) so consumers can validate payloads. SDK libraries in common languages so consumers don’t have to parse JSON manually. These tools remove friction from integration, meaning more services can successfully integrate with your webhooks.
The Future: Webhooks as Orchestration Language
Looking forward, webhooks become the glue that ties your entire development infrastructure together. Instead of separate tools that don’t talk to each other, webhooks let you build unified systems where events flow between services automatically.
Imagine: Claude Code writes a file → webhook triggers test suite → webhook sends results to Slack → webhook updates status in your project management tool → webhook triggers deployment verification → webhook alerts team if anything fails. All automatically, no manual coordination required. One action cascades through your entire infrastructure.
This is where webhook-driven architecture shines: it turns isolated tools into an integrated system. It’s powerful, but it requires thinking carefully about events, data flow, and system interactions. The patterns described here—configuration-driven dispatch, conditional routing, metrics, security, resilience—are the foundation for building sophisticated webhook ecosystems at scale.
The teams that master webhooks don’t just get better integrations. They get fundamentally different development workflows. Because when systems are connected through events, when information flows automatically between tools, the whole enterprise becomes more intelligent. Issues are caught faster because systems can react to events in real time. Deployments are safer because prerequisite checks run automatically. Teams are more effective because they’re not manually coordinating between tools—the tools coordinate automatically. That’s the real value of webhooks: not just sending notifications, but enabling intelligent, event-driven infrastructure that amplifies human capability.
-iNet