All Articles Claude Code

Slack Notification Hook: Alert Your Team from Claude Code

You're running a critical task in Claude Code—maybe it's a bulk refactor across thousands of files, or Claude is generating a comprehensive analysis that takes twenty minutes.

You’re running a critical task in Claude Code—maybe it’s a bulk refactor across thousands of files, or Claude is generating a comprehensive analysis that takes twenty minutes. Your team is waiting for the results. You don’t want to keep checking your terminal. You want them right in Slack, where everyone’s already paying attention.

Here’s the problem: notifications don’t exist in a vacuum. You don’t want to spam your team with every single operation. You want the important stuff—failures, completions of long-running tasks, validation issues. You want context: what failed, why it matters, and maybe a direct link to the failing code. And you want it formatted nicely, not as raw JSON dumps.

That’s what this article teaches you: how to build a Slack notification hook for Claude Code that sends rich, filtered, contextual alerts directly to your team.

We’ll walk through:

  • Setting up Slack webhooks (copy-paste ready)
  • Writing a notification hook in .mjs that posts to Slack
  • Filtering which events get sent (avoid alert fatigue)
  • Formatting rich Slack messages with blocks and attachments
  • Handling errors gracefully
  • Testing and debugging your hook
  • Real-world patterns for production use

By the end, you’ll have a production-ready hook that keeps your team in the loop without spamming them. -iNet

Why Slack Notifications Matter

When Claude Code runs autonomous or long-running tasks, you lose visibility if you’re not actively watching. Without notifications, you face these problems:

Problem 1: Lost productivity from waiting

You finish a task, then minimize your IDE to check Slack. You come back forty minutes later to discover Claude finished five minutes after you left. Or worse—it failed silently, and nobody noticed. This wastes time and creates frustration. If you knew immediately when tasks completed, you could move to the next one without delay, keeping your workflow continuous and efficient. You wouldn’t need to remember to check back, and you wouldn’t get surprise-blocked when a task you thought was running has actually failed.

This is more than convenience. Async team members in different timezones depend on notifications to coordinate work. When someone in Europe finishes a long-running analysis at midnight their time, they need the Asia team to know about it when they wake up. Without notifications, work stalls and context gets lost.

Problem 2: Delayed error discovery

A validation fails. A critical file wasn’t processed. But you don’t find out until the next morning when the team has already built on broken assumptions. By then, the damage is done. Commits have been made on top of invalid code. The cost of fixing it is exponentially higher than catching it when the error occurred. Slack notifications ensure errors surface immediately, when they’re easiest to fix and the blast radius is smallest. Early detection prevents cascading failures.

The worst-case scenario: a batch processing task runs overnight, encounters an error at 2am, and fails silently. Nobody notices until the next day. By then, your data is corrupted, your scheduled jobs have backed up, and your team is firefighting instead of developing. A single notification at 2am would have triggered an on-call engineer to investigate and fix it immediately.

Problem 3: Lack of team synchronization

When one person is running Claude Code tasks, the rest of the team doesn’t know what’s happening. They might start similar work, duplicating effort, or they’re blocked waiting for results they don’t know are available. Someone might spend an hour writing code that Claude just finished generating five minutes ago, but they had no visibility into it. Notifications create a shared context for the team. Everyone knows what’s in flight, what’s completed, and what’s blocked.

Imagine a feature flag team working on a new configuration system. Session A is generating the frontend code, Session B is building the backend API. Without notifications, Session B might start writing API calls to an endpoint that doesn’t exist yet, leading to wasted work. With notifications, Session B sees “API endpoint created” immediately and can start testing against real code.

Problem 4: No audit trail in your communication channel

If something goes wrong, you’re debugging from memory or digging through terminal logs. But if failures were announced in Slack, you have a searchable, threaded record of what happened and when. Slack’s archive feature means you can scroll back through the week’s notifications to understand patterns—which tasks fail regularly, which ones take longest, which events need attention. Logs are forgotten; Slack messages are discovered. This becomes invaluable for root-cause analysis and trend detection.

A team later discovered they had a recurring validation failure every Tuesday morning at 8am. They never noticed because the failures weren’t broadcast to their team channel. Once they set up notifications, they saw the pattern immediately and tracked it to a scheduled job that ran before their validation logic was ready. Quick fix.

Problem 5: Wasted context switching

Every time you leave your development environment to check if a task finished, you lose focus and context. Your brain has to reload the problem you were solving. Pulling your attention away from code to manually check progress is cognitively expensive. Notifications eliminate the need to switch—important updates come to you in the communication channel you’re already monitoring. This is the channel where your team already collaborates, so notifications fit naturally into existing workflows.

Problem 6: Missed handoffs and dependencies

In async teams across timezones, task completion notifications are essential. If someone in Europe starts a long-running analysis and needs to log off, the team in Asia needs to know when it’s done. Without notifications, work stalls waiting for manual status updates. Notifications enable true async work, where tasks complete and subsequent steps begin automatically, even across timezones and teams.

Slack notifications solve all of this. Tasks complete → team sees it immediately → team can respond instantly. Failures happen → alert goes out → team coordinates the response. Context is preserved in a searchable channel. It’s that simple. And it’s essential for teams building with Claude Code at scale.

Understanding Claude Code Notification Events

Before writing code, you need to understand what events Claude Code generates that you can hook into.

Claude Code fires notification events at critical lifecycle points:

Event Type When It Fires Typical Severity Example
task.complete When a task finishes successfully info Bulk refactor completed, 500 files processed
task.fail When a task fails error Script exited with non-zero code
validation.fail When validation fails warning or error Code style check found 42 violations
tool.error When a tool crashes error File read timeout, or API rate limit hit
subagent.complete When a spawned subagent finishes info Document analysis complete
subagent.fail When a subagent encounters an error error Subagent crashed on dependency resolution
quality.gate.fail When quality gates fail error Test coverage dropped below 80%
task.timeout When a task exceeds time limit warning Task still running after 30 minutes
resource.warning When resource usage gets high warning Memory usage at 85% of limit

Each event includes metadata:

  • eventType: The category (e.g., task.complete)
  • severity: How important (debug, info, warning, error, critical)
  • timestamp: ISO 8601 datetime when the event fired
  • source: What triggered it (task name, validation type, or component name)
  • message: Human-readable description of what happened
  • context: Additional metadata (file paths, error stack, line numbers, affected files, duration, etc.)
  • duration: How long the operation took (in milliseconds)
  • processedCount: How many items were processed (files, records, etc.)

Your hook will subscribe to specific events, filter by severity, and decide whether to send a Slack notification. This filtering is critical—if you alert on every single event, your team will tune out the notifications.

Step 1: Create a Slack Webhook

You’ll need a Slack Incoming Webhook URL to post messages. Here’s how to create one:

  1. Go to https://api.slack.com/apps and click Create New App
  2. Choose From scratch, name it “Claude Code Notifications”
  3. Select your workspace
  4. In the left sidebar, click Incoming Webhooks and toggle it On
  5. Click Add New Webhook to Workspace
  6. Select the channel where you want notifications (or create a dedicated #claude-notifications channel)
  7. You’ll get a webhook URL that looks like:
    https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX

Important: Never commit this URL to git. Store it in an environment variable:

export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX"

Or in a .env file (which you .gitignore):

SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX

Load it in your shell profile so it’s available to all processes. On macOS/Linux, add to your .bashrc or .zshrc. On Windows PowerShell, set the environment variable in system settings or in your profile script.

Step 2: Write Your First Slack Notification Hook

Now let’s write a hook that posts to Slack. Create a file called .claude/hooks/notification-slack.mjs:




/**
 * Slack Notification Hook
 * Posts Claude Code events to Slack when conditions are met
 *
 * Subscribes to: task completion, failures, validation issues
 * Severity filter: info, warning, error, critical
 */

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

if (!SLACK_WEBHOOK_URL) {
  console.error("Error: SLACK_WEBHOOK_URL environment variable is not set");
  process.exit(1);
}

// Map severity levels to Slack color codes
const severityColors = {
  debug: "#808080",
  info: "#0099FF",
  warning: "#FFAA00",
  error: "#FF3333",
  critical: "#8B0000",
};

// Parse incoming hook data from stdin
let hookData = "";

process.stdin.on("data", (chunk) => {
  hookData += chunk;
});

process.stdin.on("end", async () => {
  try {
    const event = JSON.parse(hookData);

    // Determine if we should send this notification
    const shouldNotify = shouldSendNotification(event);

    if (!shouldNotify) {
      console.log(`Skipping notification for event: ${event.eventType}`);
      process.exit(0);
    }

    // Build Slack message
    const slackMessage = buildSlackMessage(event);

    // Send to Slack
    await postToSlack(slackMessage);

    console.log(`Slack notification sent for: ${event.eventType}`);
    process.exit(0);
  } catch (error) {
    console.error(`Notification hook error: ${error.message}`);
    process.exit(1);
  }
});

/**
 * Determine if event should trigger Slack notification
 * Filter by event type and severity
 */
function shouldSendNotification(event) {
  // Only send for these event types
  const notifiableEvents = [
    "task.complete",
    "task.fail",
    "validation.fail",
    "tool.error",
    "subagent.fail",
    "quality.gate.fail",
  ];

  // Don't send debug messages
  const minimumSeverity = ["info", "warning", "error", "critical"];

  const isNotifiableEvent = notifiableEvents.includes(event.eventType);
  const isAcceptableSeverity = minimumSeverity.includes(event.severity);

  return isNotifiableEvent && isAcceptableSeverity;
}

/**
 * Build a richly-formatted Slack message with blocks
 * Uses Slack Block Kit for structure and styling
 */
function buildSlackMessage(event) {
  const color = severityColors[event.severity] || "#0099FF";
  const emoji = getEmojiForEventType(event.eventType);

  // Build the message blocks
  const blocks = [
    {
      type: "header",
      text: {
        type: "plain_text",
        text: `${emoji} ${event.source}`,
        emoji: true,
      },
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*Event:* ${event.eventType}\n*Severity:* ${event.severity}\n*Time:* ${event.timestamp}`,
      },
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `${event.message}`,
      },
    },
  ];

  // Add context details if available
  if (event.context && Object.keys(event.context).length > 0) {
    const contextLines = Object.entries(event.context)
      .map(([key, value]) => `• *${key}*: ${JSON.stringify(value)}`)
      .join("\n");

    blocks.push({
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*Details:*\n${contextLines}`,
      },
    });
  }

  // Add divider before attachment
  blocks.push({ type: "divider" });

  return {
    blocks: blocks,
    attachments: [
      {
        color: color,
        footer: "Claude Code Notifications",
        ts: Math.floor(new Date(event.timestamp).getTime() / 1000),
      },
    ],
  };
}

/**
 * Return appropriate emoji for event type
 */
function getEmojiForEventType(eventType) {
  const emojiMap = {
    "task.complete": "✅",
    "task.fail": "❌",
    "validation.fail": "⚠️",
    "tool.error": "🔴",
    "subagent.fail": "🚫",
    "quality.gate.fail": "🔒",
  };

  return emojiMap[eventType] || "📢";
}

/**
 * Post message to Slack via webhook
 */
function postToSlack(message) {
  return new Promise((resolve, reject) => {
    const url = new URL(SLACK_WEBHOOK_URL);

    const options = {
      hostname: url.hostname,
      path: url.pathname + url.search,
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Content-Length": Buffer.byteLength(JSON.stringify(message)),
      },
    };

    const req = https.request(options, (res) => {
      let data = "";

      res.on("data", (chunk) => {
        data += chunk;
      });

      res.on("end", () => {
        if (res.statusCode === 200) {
          resolve(data);
        } else {
          reject(new Error(`Slack returned status ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on("error", reject);
    req.write(JSON.stringify(message));
    req.end();
  });
}

This hook:

  • ✅ Reads notification events from stdin (Claude Code pipes them in)
  • ✅ Filters events by type and severity (only sends important alerts)
  • ✅ Builds rich Slack messages using Block Kit for visual impact
  • ✅ Posts to your Slack webhook reliably
  • ✅ Handles errors gracefully (exits without crashing)
  • ✅ Uses environment variables for secrets (never hardcoded)

Step 3: Configure the Hook in Claude Code

Register your hook in .claude/hooks/config.yaml:

notification:
  - name: slack-notifications
    handler: ./hooks/notification-slack.mjs
    events:
      - task.complete
      - task.fail
      - validation.fail
      - tool.error
      - subagent.fail
      - quality.gate.fail
    enabled: true

Now when Claude Code events fire, they’ll automatically post to Slack. The hook will receive events from Claude Code’s notification system and decide what to send based on the filters in shouldSendNotification.

Customizing Filters and Message Format

Filter by Severity

You can customize which events to send. Edit the shouldSendNotification function:

function shouldSendNotification(event) {
  // Only send errors and critical alerts (skip info/warnings)
  const strictSeverity = ["error", "critical"];

  const isAcceptableSeverity = strictSeverity.includes(event.severity);

  return isAcceptableSeverity;
}

This is helpful if you want to start strict and loosen filtering over time. Or, conversely, tighten it if you’re getting too many notifications.

Or allow only certain event types:

function shouldSendNotification(event) {
  // Only failures, ignore successes
  const failureEvents = [
    "task.fail",
    "validation.fail",
    "tool.error",
    "subagent.fail",
  ];

  return failureEvents.includes(event.eventType);
}

This is a common pattern—teams often want to know about failures immediately, but success notifications can be less urgent.

Customize Slack Formatting

Slack Block Kit is powerful. You can add:

  • Buttons to link to relevant files or logs
  • Images to display screenshots
  • Multiple sections for different pieces of information
  • Threaded replies to group related alerts

Here’s an enhanced version that adds an action button for failures:

/**
 * Build Slack message with action button for failures
 */
function buildSlackMessage(event) {
  const color = severityColors[event.severity] || "#0099FF";
  const emoji = getEmojiForEventType(event.eventType);

  const blocks = [
    {
      type: "header",
      text: {
        type: "plain_text",
        text: `${emoji} ${event.source}`,
        emoji: true,
      },
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `*Event:* ${event.eventType}\n*Severity:* ${event.severity}\n*Time:* ${event.timestamp}`,
      },
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `${event.message}`,
      },
    },
  ];

  // Add action buttons for failures
  if (event.eventType.includes("fail") || event.severity === "error") {
    blocks.push({
      type: "actions",
      elements: [
        {
          type: "button",
          text: {
            type: "plain_text",
            text: "View Details",
            emoji: true,
          },
          url: `https://your-logging-dashboard.com/events/${event.id}`,
          style: "danger",
        },
        {
          type: "button",
          text: {
            type: "plain_text",
            text: "Acknowledge",
            emoji: true,
          },
          action_id: "acknowledge_event",
        },
      ],
    });
  }

  blocks.push({ type: "divider" });

  return {
    blocks: blocks,
    attachments: [
      {
        color: color,
        footer: "Claude Code Notifications",
        ts: Math.floor(new Date(event.timestamp).getTime() / 1000),
      },
    ],
  };
}

Routing to Different Channels

What if you want different events to go to different channels? Create separate webhooks and route accordingly:

/**
 * Route to different Slack channels based on event type
 */
function getSlackWebhookForEvent(event) {
  const routingMap = {
    "quality.gate.fail": process.env.SLACK_WEBHOOK_QA,
    "task.fail": process.env.SLACK_WEBHOOK_ERRORS,
    "task.complete": process.env.SLACK_WEBHOOK_PROGRESS,
    default: process.env.SLACK_WEBHOOK_URL,
  };

  return routingMap[event.eventType] || routingMap.default;
}

Then update postToSlack to use the routed webhook:

const webhookUrl = getSlackWebhookForEvent(event);
const url = new URL(webhookUrl);
// ... rest of postToSlack function

This way, quality failures go to your QA team, task failures alert devops, and completions go to a general progress channel. Each team sees what matters to them.

Testing Your Notification Hook

You can test without actually triggering events. Create a test file test-notification.mjs:




// Sample event
const testEvent = {
  eventType: "task.fail",
  severity: "error",
  timestamp: new Date().toISOString(),
  source: "bulk-refactor-task",
  message: "Task failed: Refactoring syntax error in src/components/Header.jsx",
  context: {
    filePath: "src/components/Header.jsx",
    lineNumber: 42,
    error: "Unexpected token }",
  },
};

// Spawn hook process and pipe test data
const hook = spawn("node", ["./.claude/hooks/notification-slack.mjs"], {
  stdio: ["pipe", "inherit", "inherit"],
});

hook.stdin.write(JSON.stringify(testEvent));
hook.stdin.end();

hook.on("close", (code) => {
  console.log(`Hook exited with code: ${code}`);
});

Run it:

node test-notification.mjs

You should see a message appear in your Slack channel within seconds. If nothing appears, check environment variables and webhook configuration.

Advanced: Batching Multiple Notifications

If you’re getting flooded with notifications from a rapid sequence of events, batch them before sending:

/**
 * Batch notifications to avoid Slack spam
 */
const eventQueue = [];
let batchTimeout;

function queueNotification(event) {
  eventQueue.push(event);

  clearTimeout(batchTimeout);
  batchTimeout = setTimeout(async () => {
    if (eventQueue.length > 0) {
      const slackMessage = buildBatchedMessage(eventQueue);
      await postToSlack(slackMessage);
      eventQueue.length = 0; // Clear queue
    }
  }, 5000); // Wait 5 seconds for more events
}

function buildBatchedMessage(events) {
  const summary = events.reduce((acc, event) => {
    acc[event.eventType] = (acc[event.eventType] || 0) + 1;
    return acc;
  }, {});

  const summaryText = Object.entries(summary)
    .map(([type, count]) => `${count}x ${type}`)
    .join(" | ");

  return {
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*Batched Events (${events.length} total)*\n${summaryText}`,
        },
      },
    ],
  };
}

Advanced Notification Patterns

Pattern 1: Conditional Notifications Based on Event Context

Sometimes you only care about certain types of failures. For example, validation failures in tests don’t need notifications, but validation failures in production do:

/**
 * Smart filtering: only notify on production failures
 */
function shouldSendNotification(event) {
  // Always notify on critical issues
  if (event.severity === "critical") {
    return true;
  }

  // Only notify on validation failures in production
  if (event.eventType === "validation.fail") {
    const environment = event.context?.environment || "unknown";
    return environment === "production";
  }

  // Only notify on task failures if they're longrunning tasks
  if (event.eventType === "task.fail") {
    const duration = event.context?.durationMs || 0;
    return duration > 60000; // Only tasks over 1 minute
  }

  return false;
}

Pattern 2: Escalating Notifications

Send different messages based on failure context or retry count:

/**
 * Escalate notifications based on retry count
 */
function buildSlackMessage(event) {
  const retryCount = event.context?.retryCount || 0;
  let text = event.message;

  if (retryCount > 0) {
    text = `🔄 Retry #${retryCount}: ${event.message}`;
  }

  if (retryCount >= 3) {
    text += "\n⚠️ Multiple failures. Manual intervention may be needed.";
  }

  // ... rest of message building
}

Pattern 3: Deduplication (Preventing Duplicate Alerts)

If the same error fires twice in quick succession, you might only want one notification:

const recentEvents = new Map(); // Track recent event hashes

function shouldSendNotification(event) {
  const eventHash = hashEvent(event);
  const lastSeen = recentEvents.get(eventHash);
  const now = Date.now();

  // If we've seen this exact event in the last 5 minutes, skip it
  if (lastSeen && now - lastSeen < 5 * 60 * 1000) {
    return false;
  }

  // Record this event
  recentEvents.set(eventHash, now);

  // Clean up old entries every 100 events
  if (recentEvents.size > 100) {
    for (const [hash, timestamp] of recentEvents.entries()) {
      if (now - timestamp > 10 * 60 * 1000) {
        recentEvents.delete(hash);
      }
    }
  }

  return true;
}

function hashEvent(event) {
  return `${event.eventType}:${event.source}:${event.context?.error || ""}`;
}

Troubleshooting and Debugging

Notifications not appearing in Slack?

Step 1: Verify environment variables

echo $SLACK_WEBHOOK_URL

If this is empty, the variable isn’t set. Set it in your shell profile or .env file and reload your terminal.

Step 2: Test the webhook directly

curl -X POST -H 'Content-type: application/json' \
  --data '{"text":"Test message from curl"}' \
  $SLACK_WEBHOOK_URL

If this works, the webhook is valid. If it returns a 403 or 404, the URL is wrong or expired.

Step 3: Check hook registration
Verify the hook is in .claude/hooks/config.yaml:

notification:
  - name: slack-notifications
    handler: ./hooks/notification-slack.mjs
    enabled: true

Step 4: Check Claude Code logs
Look for errors when events fire:

tail -f ~/.claude/logs/hooks.log

Step 5: Test the hook directly
Create a test event and pipe it to your hook:

echo '{"eventType":"task.fail","severity":"error","timestamp":"2026-03-16T10:00:00Z","source":"test-task","message":"Test failure","context":{}}' | node .claude/hooks/notification-slack.mjs

Messages look broken or missing information

Problem: Fields are undefined or null
Solution: Add defensive null checks

const filePath = event.context?.filePath || "unknown";
const errorMsg = event.context?.error || "No error details";

Problem: Emojis don’t show in Slack
Solution: Ensure Block Kit blocks have emoji: true:

{
  type: "header",
  text: {
    type: "plain_text",
    text: `${emoji} ${event.source}`,
    emoji: true  // This enables emoji rendering
  }
}

Problem: Message formatting is off
Solution: Test your Block Kit JSON in Slack’s Block Kit Builder: https://app.slack.com/block-kit-builder
Paste your blocks array there to preview exactly how it will appear in Slack.

Webhook URL keeps getting blocked

Problem: “Invalid webhook URL” error
Solution:

  1. Regenerate the webhook in Slack API console
  2. Never hardcode URLs—always use environment variables
  3. If you accidentally committed the URL to git, regenerate it immediately and rotate it

Problem: “Slack returned 429” (rate limited)
Solution:

  • Slack allows 1 message per second per webhook
  • If you’re hitting this, batch notifications (combine multiple events into one message)
  • Or add a small delay between messages

Problem: “Webhook URL not found”
Solution:

  • The webhook might have expired (Slack invalidates unused webhooks after 30 days)
  • Create a new one in the Slack API console
  • Make sure you’re using the right workspace

Real-World Scenarios

Let’s look at how notification hooks solve actual problems:

Scenario 1: Multi-File Refactor with Team Visibility

Your team is refactoring a legacy module. Claude Code is running the refactor across 500 files. Without notifications:

  • Team members keep asking “is it done yet?”
  • You check the terminal every few minutes
  • When it finishes at 4:53 PM, only you know. Code review doesn’t start until tomorrow
  • If it fails at 4:47 PM, the team might call it done anyway, causing problems later

With Slack notifications:

  • Alert fires when refactor completes: “✅ Refactor complete – 500 files processed”
  • Team sees it immediately in #engineering channel
  • Code review starts within minutes, not hours
  • If it fails: “❌ Refactor failed – Syntax error in services/api.js line 234”
  • Team knows immediately and can pivot

Scenario 2: Overnight Bulk Data Processing

You schedule Claude Code to process a large dataset overnight. Normally:

  • You check logs the next morning to see what happened
  • If it failed 8 hours ago, you’ve wasted the night
  • If it succeeded but had unexpected results, you discover them during the day

With Slack notifications (routed to #data-team):

  • At 2:47 AM, notification fires: “⚠️ Data validation warning – 127 records skipped”
  • On-call engineer (if you have one) sees it and investigates
  • By morning, issues are already documented and decisions are made
  • Processing completed successfully with known anomalies

Scenario 3: Production Validation Checks

Claude Code runs validation checks on your production deployment. With notifications:

  • Critical validation failures immediately route to #incidents
  • PagerDuty gets triggered if severity is critical
  • Your team responds before customers notice
  • Audit trail shows exactly when checks ran and what they found

Best Practices for Notification Hooks

Do this:

  • ✅ Filter events by severity to reduce noise (info-level spam destroys alert value)
  • ✅ Store webhook URLs in environment variables (never in code)
  • ✅ Use descriptive event sources that identify the task/validator
  • ✅ Include context details that help debugging (file paths, line numbers, error messages)
  • ✅ Set up different channels for different event types (critical alerts vs progress updates)
  • ✅ Test hooks before deploying to production (use test events first)
  • ✅ Monitor your notification hook itself (does it have errors? Is it running?)
  • ✅ Include timestamps in messages (Slack UI adds them, but context is helpful)
  • ✅ Group related alerts (batch events that happen in quick succession)
  • ✅ Document your channel’s purpose (so team members know when to pay attention)

Don’t do this:

  • ❌ Send every single event to Slack (alert fatigue kills notification value)
  • ❌ Hardcode webhook URLs in your code (security risk, can’t rotate)
  • ❌ Log sensitive data in Slack messages (API keys, tokens, passwords)
  • ❌ Create notifications that spam threads (one notification per thread, not one per line)
  • ❌ Forget to handle errors in your hook (a broken hook silently failing is worse than no hook)
  • ❌ Set up notifications for events that happen hundreds of times (like every file processed)
  • ❌ Use rich formatting for every field (focus on signal, not noise)
  • ❌ Ignore Slack rate limits (if you post 100+ messages/minute, Slack will throttle you)

Notification-to-Signal Ratio

The most important best practice: only notify on signal, never on noise.

A well-tuned notification system fires when:

  • ✅ Important operations complete or fail
  • ✅ Unexpected conditions occur
  • ✅ Human decision/action is needed
  • ✅ Metrics cross thresholds

A poorly-tuned system fires for:

  • ❌ Every incremental step (file 1 of 500 processed)
  • ❌ Routine successes (task completed normally)
  • ❌ Debug information (function entered, data validated)
  • ❌ Unactionable information (metrics that never change)

Your team should check a notification and immediately know: “This requires my attention” or “Good to know, but doesn’t change what I’m doing right now.”

If your team starts muting the notification channel, your hook is too noisy. Recalibrate immediately. Pull back the filters, raise severity thresholds, or batch more aggressively.

Integrating with Other Services Beyond Slack

Your notification hook isn’t limited to Slack. The same pattern works for any service with webhooks.

Sending to Multiple Destinations

Post the same event to multiple services:

/**
 * Send notification to multiple destinations simultaneously
 */
async function sendNotifications(event) {
  // Don't await—fire and forget to avoid blocking
  Promise.allSettled([
    sendToSlack(event),
    sendToEmail(event),
    sendToPagerDuty(event),
  ]).catch((err) => {
    console.error(`Notification delivery error: ${err.message}`);
  });
}

async function sendToSlack(event) {
  // Existing Slack code
}

async function sendToEmail(event) {
  // Send via email service (SendGrid, AWS SES, etc.)
  const emailBody = formatEmailForEvent(event);
  // Call your email API
}

async function sendToPagerDuty(event) {
  // Only send critical failures to PagerDuty
  if (event.severity !== "critical") return;

  const pagerDutyEvent = {
    routing_key: process.env.PAGERDUTY_ROUTING_KEY,
    event_action: "trigger",
    payload: {
      summary: event.message,
      severity: event.severity,
      source: event.source,
      custom_details: event.context,
    },
  };

  await fetch("https://events.pagerduty.com/v2/enqueue", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(pagerDutyEvent),
  });
}

Discord Notifications

If your team uses Discord instead of Slack:

/**
 * Send to Discord webhook instead of Slack
 */
async function postToDiscord(event) {
  const discordMessage = {
    username: "Claude Code",
    avatar_url: "https://your-logo-url.png",
    embeds: [
      {
        title: `${event.eventType} - ${event.source}`,
        description: event.message,
        color: hexToDecimal(severityColors[event.severity]),
        timestamp: event.timestamp,
        fields: Object.entries(event.context || {}).map(([key, value]) => ({
          name: key,
          value: String(value),
          inline: true,
        })),
      },
    ],
  };

  const response = await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(discordMessage),
  });

  if (!response.ok) {
    throw new Error(`Discord returned ${response.status}`);
  }
}

function hexToDecimal(hex) {
  return parseInt(hex.replace("#", ""), 16);
}

Performance and Reliability Considerations

Async, Don’t Block

Your notification hook should never block Claude Code’s operations. Use async/await and fire-and-forget patterns:

// Good: non-blocking
async function handleEvent(event) {
  if (!shouldSendNotification(event)) {
    return;
  }

  // Fire and forget
  postToSlack(buildSlackMessage(event)).catch((err) => {
    console.error(`Failed to send notification: ${err.message}`);
    // Don't rethrow—let Claude Code continue even if notification fails
  });

  process.exit(0);
}

// Bad: blocking
async function handleEvent(event) {
  const message = buildSlackMessage(event);
  await postToSlack(message); // Waits for response—adds latency
  process.exit(0);
}

Graceful Degradation

If Slack is down, your hook shouldn’t crash. Wrap everything in error handlers:

process.stdin.on("end", async () => {
  try {
    const event = JSON.parse(hookData);

    if (!shouldSendNotification(event)) {
      process.exit(0);
    }

    const message = buildSlackMessage(event);

    // Fire and forget, don't await
    postToSlack(message).catch((err) => {
      // Log locally if Slack is down
      console.error(`Slack notification failed: ${err.message}`);
      // But still exit cleanly
    });

    // Always exit with success, even if notification fails
    process.exit(0);
  } catch (err) {
    console.error(`Notification hook error: ${err.message}`);
    // Exit with non-zero to signal error, but don't crash
    process.exit(1);
  }
});

Organizational Learning Through Notifications

One underappreciated benefit of notification systems is that they create a searchable record of events. In three months, when you’re asking “how often does this validation fail?”, you can search your Slack history and have the answer immediately. Teams that track important events in Slack have an institutional memory that teams without notifications simply can’t access. You can look back at patterns: “Last quarter, we had 47 validation failures. This quarter, only 3. Something we did is working.” Or conversely: “Failures are trending upward. We need to investigate.”

This historical perspective is invaluable for understanding whether your systems are getting more reliable or less. It helps you make data-driven decisions about where to invest engineering effort. It shows stakeholders that you’re tracking reliability seriously. And it creates accountability: if you say “we fixed the intermittent timeout issue,” you can point to the event history and show that timeouts have decreased.

Some teams take this further and build dashboards that pull from notification histories. Weekly summaries of events by type and severity. Trend graphs showing whether system reliability is improving. These become part of the team’s regular reviews. The notifications that seemed like just a convenience become organizational feedback mechanisms that drive technical decisions.

What’s Next?

Now that you have notifications flowing to Slack, consider:

  1. Add metrics dashboard – Build a summary of daily events in a Slack workflow
  2. Set up escalation policies – Critical alerts to PagerDuty, warnings to Slack, debug info to logs
  3. Route by team – Different channels for different teams (QA, DevOps, Frontend, etc.)
  4. Build interactive responses – Add Slack buttons that trigger follow-up Claude Code tasks
  5. Implement a question-answering bot – Slack bot that queries Claude Code’s event history
  6. Correlate with metrics – Link notifications to system dashboards showing performance at event time

Your team is now connected to Claude Code. They see when tasks complete, when things fail, and they can respond immediately instead of discovering issues hours later. Notifications are the bridge between autonomous AI systems and human awareness. They transform Claude Code from a tool you check on occasionally into a collaborator who keeps the team informed in real-time.

The key insight: visibility without noise. Well-tuned notifications make your team more efficient. Over-tuned notifications make them ignore the channel. Get the balance right, and you’ve built a force multiplier for your entire team. The team that knows immediately when tasks complete can iterate faster. The team that discovers issues hours later is constantly playing catch-up. The difference is just good notification hygiene.

Advanced Integration Patterns

As you mature your notification system, consider more sophisticated patterns that go beyond simple alerts.

Context-Aware Notifications

Different team members care about different events. A backend engineer cares when API generation completes. A QA engineer cares when test suites finish. A DevOps engineer cares when deployments succeed or fail. Rather than sending everyone every notification, build context-aware routing. Check if an engineer is on-call for a service, and only notify them about incidents related to that service. Check their timezone and batch notifications so they’re delivered during working hours rather than the middle of the night.

/**
 * Route notifications based on team member expertise and schedule
 */
function routeNotificationToTeamMembers(event, teamMembers) {
  const recipients = [];

  for (const member of teamMembers) {
    // Only notify if they own this service or are on-call for it
    if (!member.services.includes(event.service) && !isOnCall(member)) {
      continue;
    }

    // Only notify during their working hours
    if (!isWithinWorkingHours(member.timezone)) {
      queueNotificationForLaterDelivery(member, event);
      continue;
    }

    // If already notified about similar issue recently, don't spam
    if (hasRecentNotification(member, event.type)) {
      continue;
    }

    recipients.push(member.slackId);
  }

  return recipients;
}

This transforms notifications from broadcast (everyone gets everything) to targeted (right person, right time, right context).

Notification Threads and Conversation

Slack threads are powerful for grouping related notifications. Instead of having independent alerts scattered across a channel, group them into threads:

/**
 * Group related notifications into threads
 */
function buildThreadedNotification(event, previousEvent) {
  const baseMessage = buildSlackMessage(event);

  // If this is a follow-up event (e.g., failure and then retry),
  // add it to the thread of the previous event
  if (previousEvent && shouldGroupIntoThread(event, previousEvent)) {
    baseMessage.thread_ts = previousEvent.slackTimestamp;
    baseMessage.reply_broadcast = false; // Only visible in thread, not in main channel
  } else {
    // New incident, broadcast to channel
    baseMessage.reply_broadcast = true;
  }

  return baseMessage;
}

This creates a conversation structure. Someone can follow a thread to understand the full lifecycle of a task: it started, it completed, it failed, it was retried, it succeeded. The channel stays cleaner because follow-ups are in threads rather than separate messages.

Interactive Notifications with Actions

Slack allows interactive buttons and menus in notifications. Use these to enable immediate action without leaving Slack:

/**
 * Build notifications with contextual actions
 */
function buildInteractiveNotification(event) {
  const blocks = buildBaseNotificationBlocks(event);

  // Add contextual actions based on event type
  if (event.eventType === "task.fail") {
    blocks.push({
      type: "actions",
      elements: [
        {
          type: "button",
          text: { type: "plain_text", text: "View Logs" },
          url: `https://your-logging-dashboard.com/task/${event.taskId}`,
        },
        {
          type: "button",
          text: { type: "plain_text", text: "Retry Task" },
          action_id: "retry_task",
          value: event.taskId,
        },
        {
          type: "button",
          text: { type: "plain_text", text: "Acknowledge" },
          action_id: "acknowledge_failure",
          value: event.taskId,
        },
      ],
    });
  }

  return { blocks };
}

When an engineer sees a failure notification, they can immediately click “Retry” or “View Logs” without context switching. This dramatically reduces friction in incident response.

Smart Batching During High-Traffic Periods

During incidents when events are firing rapidly, batching becomes critical:

/**
 * Intelligent batching prevents Slack notification spam
 */
const eventBatch = [];
const batchConfig = {
  maxWaitTime: 30000, // 30 seconds max before sending
  maxBatchSize: 10, // Or 10 events, whichever comes first
  minWaitTime: 5000, // Wait at least 5 seconds to accumulate
};

function addEventToBatch(event) {
  eventBatch.push(event);

  if (eventBatch.length >= batchConfig.maxBatchSize) {
    flushBatch(); // Send immediately if batch is full
  } else if (eventBatch.length === 1) {
    // Set timer for first event in batch
    setTimeout(flushBatch, batchConfig.maxWaitTime);
  }
}

function flushBatch() {
  if (eventBatch.length === 0) return;

  const summary = generateBatchSummary(eventBatch);
  postToSlack(summary);
  eventBatch.length = 0;
}

During an outage, instead of sending 100 individual messages about individual failures, you send batched updates: “27 task failures in auth-service, 15 in payment-service, 8 in checkout-service.” Much more digestible.

Handling Edge Cases and Failures

Your notification system needs to be resilient. What happens if Slack is down? What if your hook crashes? What if the webhook URL becomes invalid?

/**
 * Resilient notification system with fallbacks
 */
async function sendNotificationWithFallbacks(event) {
  // Primary: Slack
  try {
    await sendToSlack(event);
    return;
  } catch (error) {
    console.error(`Slack notification failed: ${error.message}`);
  }

  // Fallback 1: Email (slower but more reliable)
  try {
    await sendEmail(event);
    return;
  } catch (error) {
    console.error(`Email notification failed: ${error.message}`);
  }

  // Fallback 2: Log locally for manual review
  try {
    logToFile(event);
    return;
  } catch (error) {
    console.error(
      `Even logging failed. Notification system is broken: ${error.message}`,
    );
  }
}

This layered approach ensures that critical notifications reach people even if your primary channel fails. Slack down? Email still works. Email provider down? Logs are still local. The cascade of fallbacks ensures you never lose visibility of critical events.

Webhook URL Rotation and Validation

Slack webhook URLs can expire or be rotated for security. Validate URLs and handle rotation gracefully:

/**
 * Validate and maintain webhook URLs
 */
class WebhookManager {
  constructor() {
    this.webhooks = new Map(); // channel -> webhook URL
    this.lastValidated = new Map();
    this.validationInterval = 60 * 60 * 1000; // 1 hour
  }

  async getValidWebhook(channel) {
    const webhook = this.webhooks.get(channel);
    const lastValidated = this.lastValidated.get(channel) || 0;

    if (!webhook) {
      throw new Error(`No webhook configured for ${channel}`);
    }

    // Validate webhook if not checked recently
    if (Date.now() - lastValidated > this.validationInterval) {
      const isValid = await this.validateWebhook(webhook);
      if (!isValid) {
        throw new Error(
          `Webhook for ${channel} is no longer valid. Regenerate in Slack API console.`,
        );
      }
      this.lastValidated.set(channel, Date.now());
    }

    return webhook;
  }

  async validateWebhook(url) {
    try {
      const response = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ text: "" }), // Empty test message
      });
      return response.status === 200;
    } catch (error) {
      return false;
    }
  }
}

This prevents silent failures where notifications are being sent but dropped because the webhook is invalid.

Monitoring Your Notification System

Your notification system itself needs monitoring. If the hook crashes, who alerts about that?

/**
 * Monitor notification delivery success rate
 */
class NotificationMetrics {
  constructor() {
    this.sent = 0;
    this.failed = 0;
    this.lastReset = Date.now();
  }

  recordSuccess() {
    this.sent++;
    this.checkHealthStatus();
  }

  recordFailure() {
    this.failed++;
    this.checkHealthStatus();
  }

  checkHealthStatus() {
    const successRate = this.sent / (this.sent + this.failed);

    if (successRate < 0.95) {
      // Less than 95% success rate indicates a problem
      console.error(
        `NOTIFICATION SYSTEM DEGRADED: ${(successRate * 100).toFixed(1)}% success rate`,
      );
      // Alert your ops team
      alertOps("Notification system below SLA");
    }
  }

  getMetrics() {
    return {
      total: this.sent + this.failed,
      sent: this.sent,
      failed: this.failed,
      successRate:
        ((this.sent / (this.sent + this.failed)) * 100).toFixed(1) + "%",
    };
  }
}

Include notification system health in your dashboards. Track success rate, latency, and delivery timeliness. If your notification system fails silently, you’ve lost visibility of all downstream problems.

Notification Strategies for Different Team Sizes

Notification needs scale differently for different team sizes:

Small teams (< 10 people): Send everything to one shared channel. Everyone sees everything. Simple and direct, but can be noisy.

Medium teams (10-50 people): Segment by function. Frontend team gets frontend notifications, backend team gets backend notifications. Critical alerts go to a shared incident channel.

Large teams (50+ people): Segment by service and expertise. Use intelligent routing so engineers get notifications relevant to what they own. Route based on on-call schedule. Use interactive actions to let engineers claim and resolve incidents from notifications.

Organizations (100+ people): Build a notification platform that other teams use. Provide templating, routing, batching, and fallbacks as a shared service. Different teams can customize but infrastructure is centralized. This prevents notification proliferation and ensures consistent quality.

The Psychological Impact of Notifications

One final perspective: notifications affect team culture and psychology. Well-designed notifications make your team feel informed and in control. They see their work progressing in real-time. They know immediately when something breaks.

Poorly-designed notifications create anxiety. Your team gets paged for false alarms repeatedly, so they stop trusting alerts. They’re constantly context-switching to check notifications. They never have uninterrupted focus time.

The difference is subtle but profound. A well-notified team is engaged and responsive. A poorly-notified team is burned out and cynical. Much of this difference comes down to notification quality.

Invest in getting notifications right. It affects not just incident response speed but team morale and productivity.

-iNet

Free Discovery Call

Start With a Conversation, Not a Commitment

Every engagement begins with a free 30-minute discovery call. We'll map what's slowing your business down and tell you exactly what we'd fix first – no pitch deck, no obligation.