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
.mjsthat 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:
- Go to https://api.slack.com/apps and click Create New App
- Choose From scratch, name it “Claude Code Notifications”
- Select your workspace
- In the left sidebar, click Incoming Webhooks and toggle it On
- Click Add New Webhook to Workspace
- Select the channel where you want notifications (or create a dedicated
#claude-notificationschannel) - 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:
- Regenerate the webhook in Slack API console
- Never hardcode URLs—always use environment variables
- 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:
- Add metrics dashboard – Build a summary of daily events in a Slack workflow
- Set up escalation policies – Critical alerts to PagerDuty, warnings to Slack, debug info to logs
- Route by team – Different channels for different teams (QA, DevOps, Frontend, etc.)
- Build interactive responses – Add Slack buttons that trigger follow-up Claude Code tasks
- Implement a question-answering bot – Slack bot that queries Claude Code’s event history
- 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