Powiadomienia przez hooki: Claude sam powie, kiedy skończył

Skonfiguruj Stop Hook, aby Claude automatycznie powiadamiał cię przez pulpit, Slack lub Telegram po zakończeniu zadania


Claude Code często potrzebuje kilku minut — albo i dłużej — żeby ukończyć zadanie. Prosisz go o refaktoryzację modułu, odpalenie testów czy przetworzenie paczki plików, a sam przełączasz się na coś innego. Problem? Nie wiesz, kiedy skończył.

Wracasz, a on może już od dziesięciu minut czeka z wynikami. Albo utknął na okienku z potwierdzeniem i czeka na ciebie.

Hook Stop pozwala automatycznie dostawać powiadomienie, gdy Claude skończy pracę lub będzie potrzebował twojej reakcji. Nieważne, w jakim jesteś oknie czy na jakim urządzeniu — powiadomienie do ciebie dotrze.


Mechanizm działania

Claude Code udostępnia zdarzenie Stop — odpala się za każdym razem, gdy Claude się zatrzymuje: skończył zadanie, napotkał błąd albo czeka na input od użytkownika.

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

Hook dostaje na stdin JSON z polami stop_reason (powód zatrzymania) i stop_message (ostatnia wiadomość Claude'a). Na ich podstawie można dostosować treść powiadomienia.

Struktura JSON na stdin:

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

Opcja 1: systemowe powiadomienia na pulpicie (najprostsza)

Na Linuksie — notify-send, na macOS — osascript. Żadnych zewnętrznych serwisów:

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

Zaleta: zero konfiguracji, działa od razu.
Wada: powiadomienie przychodzi tylko na bieżącą maszynę — na innym urządzeniu go nie zobaczysz.


Opcja 2: Slack Webhook (polecana dla zespołów)

Slack Incoming Webhook pozwala wysyłać powiadomienia na wybrany kanał lub w wiadomości prywatnej.

Przygotowanie:
1. Stwórz aplikację na stronie zarządzania App w Slacku
2. Włącz Incoming Webhooks i skopiuj URL webhooka
3. Zapisz URL w zmiennej środowiskowej 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 &

& na końcu jest kluczowe — curl działa w tle i nie blokuje Claude'a.


Opcja 3: bot na Telegramie

Bot telegramowy świetnie sprawdza się do użytku osobistego — za darmo, natychmiast, powiadomienia trafiają prosto na telefon.

Przygotowanie:
1. Stwórz bota przez @BotFather i zdobądź token
2. Ustal swój Chat ID (wyślij botowi wiadomość, potem wywołaj API getUpdates)
3. Ustaw zmienne środowiskowe TELEGRAM_BOT_TOKEN i 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 &

Opcja 4: sygnał dźwiękowy (przydatny też przez SSH)

Jeśli jesteś na tej samej maszynie, ale przełączyłeś okno, dźwięk często działa lepiej niż wyskakujące powiadomienie:

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

Można łączyć z innymi opcjami — lokalny dźwięk plus zdalny push.


Pełna konfiguracja: łączymy kilka sposobów

W praktyce najlepiej zebrać wszystko w jednym skrypcie:

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

Kilka uwag

Wszystkie zapytania sieciowe — w tle

Każdy curl w skrypcie musi mieć & na końcu. Jeśli sieć zwolni albo nastąpi timeout, zablokuje to całą sesję Claude'a. Wzorzec > /dev/null 2>&1 & to standard.

Stop nie zawsze znaczy „ukończone pomyślnie"

stop_reason o wartości end_turn zazwyczaj oznacza, że zadanie zostało wykonane, ale czasem Claude po prostu nie wie, co robić dalej. Jeśli chcesz dostawać powiadomienia tylko przy rzeczywistym zakończeniu, dodaj filtr:

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

Przycinaj treść powiadomienia

Ostatnia wiadomość Claude'a może być bardzo długa (bloki kodu, duży output). Przed wysłaniem do Slacka lub Telegrama przytnij ją przez head -c 300, żeby nie dostawać ścian tekstu.

Zmienne środowiskowe — poza repozytorium

URL webhooka i tokeny botów nie powinny trafiać do kodu projektu. Trzymaj je w ~/.zshrc (lub ~/.bashrc) albo zarządzaj nimi przez direnv.


Efekt

Po skonfigurowaniu twój sposób pracy się zmienia:

  1. Dajesz Claude'owi zadanie
  2. Przełączasz się na inne okno lub urządzenie
  3. Claude kończy → przychodzi powiadomienie
  4. Wracasz, sprawdzasz wynik, idziesz dalej

Koniec z wpatrywaniem się w terminal, koniec z ciągłym przełączaniem okien, żeby sprawdzić postęp. Claude robi swoje, ty robisz swoje, a jak skończy — sam ci powie.