用 Hooks 发任务完成通知:Claude 干完活自动告诉你

配置 Stop Hook,让 Claude 完成任务后通过桌面通知、Slack、Telegram 自动推送消息


Claude Code 执行任务经常需要几分钟甚至更长。你让它重构一个模块、跑完测试套件、处理一批文件——然后切窗口去做别的事。问题是:你不知道它什么时候干完了。

回来一看,可能已经完成了十分钟,也可能卡在一个确认弹窗上等你。

Stop Hook 可以在 Claude 完成任务(或需要你介入)时自动发通知。不管你在哪个窗口、哪台设备,通知直接推过来。


核心机制

Claude Code 有一个 Stop 事件——每次 Claude 停下来时触发,不管是任务完成、遇到错误需要确认,还是等待用户输入。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/notify.sh"
          }
        ]
      }
    ]
  }
}

Hook 通过 stdin 拿到一个 JSON,里面包含 stop_reason(停止原因)和 stop_message(Claude 最后说的话)。根据这些信息,你可以定制通知内容。

stdin 数据结构:

{
  "session_id": "xxx",
  "stop_reason": "end_turn",
  "stop_message": "重构完成,所有测试通过。"
}

方案一:系统桌面通知(最简单)

Linux 用 notify-send,macOS 用 osascript,不需要任何外部服务:

#!/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

优点:零配置,即开即用。
缺点:只有当前机器能收到,切到其他设备就看不到了。


方案二:Slack Webhook(推荐团队使用)

用 Slack Incoming Webhook 把通知推到指定频道或私信。

前置准备:
1. 在 Slack 的 App 管理页面 创建一个 App
2. 启用 Incoming Webhooks,拿到 Webhook URL
3. 把 URL 存到环境变量 SLACK_WEBHOOK_URL

#!/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 &

末尾的 & 很重要——让 curl 在后台执行,不阻塞 Claude。


方案三:Telegram Bot

Telegram Bot 适合个人使用,免费、即时,手机上就能收到。

前置准备:
1. 找 @BotFather 创建 Bot,拿到 Token
2. 获取你的 Chat ID(给 Bot 发条消息,然后调用 getUpdates API)
3. 设置环境变量 TELEGRAM_BOT_TOKENTELEGRAM_CHAT_ID

#!/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 &

方案四:声音提示(SSH 场景也好用)

如果你在同一台机器上但切了窗口,一声提示音比弹窗更有效:

#!/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

可以和其他方案组合——本地播放声音 + 远程推送通知。


完整配置:组合多种通知方式

实际使用中,建议把多种方式组合到一个脚本里:

.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"
          }
        ]
      }
    ]
  }
}

几个注意事项

所有网络请求都要后台执行

通知脚本里的 curl 必须加 & 放后台。如果网络慢或者超时,会卡住 Claude 的整个会话。加 > /dev/null 2>&1 & 是标准做法。

Stop 不等于"成功完成"

stop_reasonend_turn 通常表示任务完成,但也可能是 Claude 不确定下一步该做什么。如果你只想在真正完成时通知,可以在脚本里过滤:

[[ "$reason" != "end_turn" ]] && exit 0

通知内容要截断

Claude 的最后一条消息可能很长(包含代码块、大段输出)。推送到 Slack 或 Telegram 之前,用 head -c 300 截断,避免消息爆炸。

环境变量建议放全局配置

Webhook URL 和 Bot Token 不应该写进项目仓库。建议放在 ~/.zshrc(或 ~/.bashrc)里,或者用 direnv 管理。


效果

配置之后,你的工作方式变了:

  1. 给 Claude 一个任务
  2. 切到其他窗口/设备
  3. Claude 干完 → 通知推送过来
  4. 回来看结果,继续下一步

不用盯着终端等,不用反复切窗口看进度。Claude 干活,你干你的,完事通知你。