Уведомления через хуки: Claude сам скажет, когда закончил

Настройте Stop Hook, чтобы Claude автоматически уведомлял вас через рабочий стол, Slack или Telegram по завершении задачи


Claude Code часто работает несколько минут, а то и дольше. Вы просите его отрефакторить модуль, прогнать тесты, обработать пачку файлов — и переключаетесь на другие дела. Проблема в том, что вы не знаете, когда он закончит.

Возвращаетесь — а он, может, уже десять минут как всё сделал. Или завис на окне подтверждения и ждёт вас.

С хуком Stop можно получать уведомление автоматически, как только Claude завершил задачу или ждёт вашего вмешательства. Неважно, в каком вы окне или на каком устройстве — уведомление придёт.


Как это работает

В Claude Code есть событие Stop — оно срабатывает каждый раз, когда Claude останавливается: завершил задачу, наткнулся на ошибку или ждёт пользовательского ввода.

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

Хук получает на stdin JSON с полями stop_reason (причина остановки) и stop_message (последнее сообщение Claude). На основе этих данных можно настраивать содержимое уведомления.

Структура JSON на stdin:

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

Вариант 1: системные уведомления на десктопе (самый простой)

На 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

Плюс: нулевая настройка, работает из коробки.
Минус: уведомление приходит только на текущую машину — на другом устройстве вы его не увидите.


Вариант 2: Slack Webhook (рекомендуется для команд)

Через Slack Incoming Webhook можно отправлять уведомления в нужный канал или в личные сообщения.

Подготовка:
1. Создайте приложение на странице управления App в Slack
2. Включите Incoming Webhooks и скопируйте 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.


Вариант 3: Telegram-бот

Telegram-бот отлично подходит для личного использования — бесплатно, мгновенно, уведомления приходят прямо на телефон.

Подготовка:
1. Создайте бота через @BotFather и получите токен
2. Узнайте свой Chat ID (отправьте боту сообщение, затем вызовите API getUpdates)
3. Задайте переменные окружения TELEGRAM_BOT_TOKEN и TELEGRAM_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 &

Вариант 4: звуковой сигнал (удобно и через 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_reason со значением end_turn обычно означает, что задача выполнена, но иногда Claude просто не знает, что делать дальше. Если хотите получать уведомления только при реальном завершении, добавьте фильтр:

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

Обрезайте текст уведомления

Последнее сообщение Claude может быть очень длинным (блоки кода, объёмный вывод). Перед отправкой в Slack или Telegram обрезайте его через head -c 300, чтобы не получать простыни текста.

Переменные окружения — вне репозитория

Webhook URL и токены ботов не должны попадать в код проекта. Храните их в ~/.zshrc (или ~/.bashrc) либо используйте direnv.


Результат

После настройки ваш рабочий процесс меняется:

  1. Даёте Claude задачу
  2. Переключаетесь на другое окно или устройство
  3. Claude заканчивает → приходит уведомление
  4. Возвращаетесь, смотрите результат, продолжаете

Не нужно сидеть и пялиться в терминал, не нужно постоянно переключаться, чтобы проверить прогресс. Claude работает, вы занимаетесь своими делами, а когда он закончит — сам сообщит.