Set up a Stop Hook so Claude automatically notifies you via desktop, Slack, or Telegram when it finishes a task
Claude Code tasks often take several minutes or longer. You kick off a module refactor, a full test suite, or a batch file operation — then switch to another window and move on with your day. The problem: you have no idea when it's done.
You check back and it might have finished ten minutes ago. Or it might be sitting there waiting for your confirmation.
With the Stop Hook, you can get notified automatically whenever Claude finishes a task or needs your input. The notification reaches you no matter which window or device you're on.
Claude Code exposes a Stop event that fires every time Claude stops — whether it completed the task, hit an error that needs confirmation, or is waiting for user input.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/notify.sh"
}
]
}
]
}
}
The Hook receives a JSON payload via stdin containing stop_reason (why it stopped) and stop_message (Claude's last message). You can use these to customize your notification.
stdin payload:
{
"session_id": "xxx",
"stop_reason": "end_turn",
"stop_message": "重构完成,所有测试通过。"
}
Use notify-send on Linux or osascript on macOS — no external services required:
#!/bin/bash
input=$(cat)
reason=$(echo "$input" | jq -r '.stop_reason // "unknown"')
message=$(echo "$input" | jq -r '.stop_message // "Claude 已停止"' | head -c 200)
case "$(uname)" in
Linux)
notify-send "Claude Code" "$message" --urgency=normal
;;
Darwin)
osascript -e "display notification \"$message\" with title \"Claude Code\""
;;
esac
Pros: Zero configuration, works out of the box.
Cons: Only works on the local machine — you won't see it if you switch to another device.
Push notifications to a Slack channel or DM using an Incoming Webhook.
Setup:
1. Create an app in the Slack App Management page
2. Enable Incoming Webhooks and grab the Webhook URL
3. Store the URL in the SLACK_WEBHOOK_URL environment variable
#!/bin/bash
input=$(cat)
reason=$(echo "$input" | jq -r '.stop_reason // "unknown"')
message=$(echo "$input" | jq -r '.stop_message // "Claude 已停止"' | head -c 300)
webhook_url="${SLACK_WEBHOOK_URL}"
[[ -z "$webhook_url" ]] && exit 0
# 根据停止原因选 emoji
case "$reason" in
end_turn) emoji="✅" ;;
user_input) emoji="⏳" ;;
*) emoji="🔔" ;;
esac
payload=$(jq -n \
--arg text "$emoji *Claude Code* | \`$reason\`
$message" \
'{text: $text}')
curl -s -X POST "$webhook_url" \
-H 'Content-Type: application/json' \
-d "$payload" > /dev/null 2>&1 &
The trailing & matters — it runs curl in the background so it doesn't block Claude.
Telegram bots are a great fit for personal use: free, instant, and notifications land right on your phone.
Setup:
1. Create a bot through @BotFather and grab the token
2. Get your Chat ID (send a message to the bot, then call the getUpdates API)
3. Set the TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables
#!/bin/bash
input=$(cat)
reason=$(echo "$input" | jq -r '.stop_reason // "unknown"')
message=$(echo "$input" | jq -r '.stop_message // "Claude 已停止"' | head -c 300)
token="${TELEGRAM_BOT_TOKEN}"
chat_id="${TELEGRAM_CHAT_ID}"
[[ -z "$token" || -z "$chat_id" ]] && exit 0
text="🤖 *Claude Code*
状态:\`$reason\`
$message"
curl -s "https://api.telegram.org/bot${token}/sendMessage" \
-d chat_id="$chat_id" \
-d text="$text" \
-d parse_mode="Markdown" > /dev/null 2>&1 &
If you're on the same machine but in a different window, an audio cue can be more effective than a popup:
#!/bin/bash
input=$(cat)
reason=$(echo "$input" | jq -r '.stop_reason // "unknown"')
case "$(uname)" in
Linux)
paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null &
;;
Darwin)
afplay /System/Library/Sounds/Glass.aiff &
;;
esac
This works well in combination with other options — play a sound locally while also pushing a remote notification.
In practice, you'll want to combine multiple notification methods in a single script:
.claude/hooks/notify.sh:
#!/bin/bash
input=$(cat)
reason=$(echo "$input" | jq -r '.stop_reason // "unknown"')
message=$(echo "$input" | jq -r '.stop_message // "Claude 已停止"' | head -c 300)
# 根据原因选 emoji
case "$reason" in
end_turn) emoji="✅"; title="任务完成" ;;
user_input) emoji="⏳"; title="等待输入" ;;
*) emoji="🔔"; title="Claude 已停止" ;;
esac
# 1. 桌面通知(本地)
case "$(uname)" in
Linux) notify-send "Claude Code: $title" "$message" --urgency=normal ;;
Darwin) osascript -e "display notification \"$message\" with title \"Claude Code: $title\"" ;;
esac
# 2. 声音提示
case "$(uname)" in
Linux) paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null & ;;
Darwin) afplay /System/Library/Sounds/Glass.aiff & ;;
esac
# 3. Slack(如果配了 Webhook)
if [[ -n "$SLACK_WEBHOOK_URL" ]]; then
payload=$(jq -n --arg text "$emoji *$title*
$message" '{text: $text}')
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$payload" > /dev/null 2>&1 &
fi
# 4. Telegram(如果配了 Bot)
if [[ -n "$TELEGRAM_BOT_TOKEN" && -n "$TELEGRAM_CHAT_ID" ]]; then
curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="$TELEGRAM_CHAT_ID" \
-d text="$emoji *$title*
$message" \
-d parse_mode="Markdown" > /dev/null 2>&1 &
fi
.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/notify.sh"
}
]
}
]
}
}
Always run network requests in the background
Every curl call in your notification script should end with &. If the network is slow or the request times out, it will block Claude's entire session. The pattern > /dev/null 2>&1 & is the standard approach.
"Stop" doesn't always mean "successfully completed"
A stop_reason of end_turn usually means the task is done, but it can also mean Claude isn't sure what to do next. If you only want notifications on true completion, filter in your script:
[[ "$reason" != "end_turn" ]] && exit 0
Truncate notification content
Claude's last message can be very long — full of code blocks and verbose output. Truncate it with head -c 300 before sending to Slack or Telegram to avoid message overload.
Keep secrets out of your repo
Webhook URLs and bot tokens don't belong in your project repository. Store them in ~/.zshrc (or ~/.bashrc), or manage them with direnv.
Once this is set up, your workflow changes:
No more staring at the terminal. No more tab-switching to check progress. Claude works, you work, and it pings you when it's done.