Send Claude Code alerts to Slack or Discord
If you live in Slack or Discord, that is where you want Claude Code to reach you, especially when it finishes a long run while you are in a meeting, or away from the machine entirely. Both take an incoming webhook, and a Claude Code hook is just a shell command, so curl is all you need.
New to hooks? Start with the complete guide to Claude Code hooks. Otherwise, here is the wiring.
Get a webhook URL
- Slack: create an Incoming Webhook for the channel you want (Slack app settings → Incoming Webhooks). You get a URL like
https://hooks.slack.com/services/.... - Discord: channel → Edit Channel → Integrations → Webhooks → New Webhook, then Copy Webhook URL.
Both URLs are credentials: anyone holding one can post to your channel. Treat them accordingly (see below).
Slack
Post a message when a run finishes, using the Stop hook:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "curl -s -X POST -H 'Content-type: application/json' -d '{\"text\":\"✅ Claude Code finished a run\"}' \"$SLACK_WEBHOOK_URL\"" }
]
}
]
}
}
Keep the URL out of the file: export SLACK_WEBHOOK_URL in your shell profile and reference it, so you are not committing a secret to .claude/settings.json. Hooks do not load your interactive profile, so if the variable comes up empty, set it through the env key of your settings instead:
{
"env": { "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/..." }
}
Put that in ~/.claude/settings.json or .claude/settings.local.json, never in the project file you commit.
Discord
Discord webhooks take a content field:
{
"hooks": {
"Notification": [
{
"hooks": [
{ "type": "command", "command": "curl -s -X POST -H 'Content-type: application/json' -d '{\"content\":\"🙋 Claude Code needs your input\"}' \"$DISCORD_WEBHOOK_URL\"" }
]
}
]
}
}
Wire Notification for "needs you" and Stop for "done", and you get both moments in your channel.
Say which project it was
A channel full of identical "finished" lines is barely better than no channel at all. The hook payload arriving on stdin carries the session's working directory and id, so a short script can label the message:
#!/usr/bin/env bash
# .claude/hooks/slack.sh usage: slack.sh "finished"
payload=$(cat)
project=$(basename "$(echo "$payload" | jq -r '.cwd')")
text="Claude Code: *${project}* $1"
curl -s -X POST -H 'Content-type: application/json' \
-d "$(jq -n --arg t "$text" '{text: $t}')" \
"$SLACK_WEBHOOK_URL" > /dev/null
Building the JSON with jq -n rather than string concatenation matters: a project name with a quote or a backslash would otherwise produce invalid JSON and a silent failure.
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/slack.sh finished", "timeout": 10 }] }
],
"Notification": [
{ "hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/slack.sh 'needs you'", "timeout": 10 }] }
]
}
}
Test the webhook before wiring it
A hook that fails prints nothing you will notice, so prove the URL works from a plain shell first:
curl -s -X POST -H 'Content-type: application/json' \
-d '{"text":"hello from my machine"}' "$SLACK_WEBHOOK_URL"
Slack answers ok and Discord answers with an empty 204. Anything else (invalid_payload, no_service) is a problem with the URL or the JSON, not with Claude Code. Once that works, add the hook and run claude --debug to watch it fire.
Richer Slack messages
Plain text is fine, but Slack's Block Kit lets you make the project name scannable, which matters once several repositories post to the same channel:
#!/usr/bin/env bash
payload=$(cat)
project=$(basename "$(echo "$payload" | jq -r '.cwd')")
jq -n --arg p "$project" '{
blocks: [
{ type: "section", text: { type: "mrkdwn", text: ("*" + $p + "* finished a run") } },
{ type: "context", elements: [ { type: "mrkdwn", text: "Claude Code" } ] }
]
}' | curl -s -X POST -H 'Content-type: application/json' -d @- "$SLACK_WEBHOOK_URL" > /dev/null
Discord has its own version of this with embeds, taking a title, a description and a color, posted to the same webhook URL. Either way, resist the urge to build a dashboard in chat: the message exists to make you look, not to replace the terminal.
Keep the channel quiet enough to matter
Every short turn firing a webhook makes the channel unreadable within a day, and Slack and Discord both throttle webhooks that post too often. Two habits keep it useful: post Notification events always (something is blocking you), and gate Stop on the run having taken more than a minute, by stamping the start time in a UserPromptSubmit hook and comparing it in the Stop one. A dedicated, muted channel that you check on purpose beats a busy one you learn to ignore.
Keep the payload dumb
Do not send the prompt, the diff, or anything Claude typed to a chat channel: it is easy to leak more than you mean to, and the hook payload happily hands you prompt and transcript_path if you ask for them. A short, fixed message ("finished", "needs you") plus a project name is enough to make you look, which is the whole point. Remember also that a project-level hook runs on your teammates' machines too, so a webhook committed in .claude/settings.json means everyone posts to your channel.
For your own screen, not just the channel
A channel ping is great when you are away. When you are at your machine running several sessions, you want something faster than switching to Slack and working out which project pinged. Blooby reads the same Claude Code events and shows a live mascot per session right on your desktop (done, needs-you, working), so you never leave your editor to find out. Use both: a webhook for when you are out, and Blooby for when you are in.
See it for yourself
download Get Blooby free