Notifiche automatiche con gli Hooks: fatti avvisare quando Claude ha finito

Configura uno Stop Hook per ricevere notifiche automatiche via desktop, Slack o Telegram quando Claude finisce un task


Claude Code impiega spesso diversi minuti — o anche di più — per completare un'attività. Gli chiedi di fare un refactoring, di eseguire la suite di test o di elaborare un batch di file, e nel frattempo passi a fare altro. Il problema? Non sai quando ha finito.

Quando torni, magari ha già completato da dieci minuti. O peggio: è fermo su una richiesta di conferma e sta aspettando te.

Con l'hook Stop puoi ricevere una notifica automatica ogni volta che Claude termina un'attività o ha bisogno del tuo intervento. Qualunque finestra o dispositivo tu stia usando, la notifica ti raggiunge.


Come funziona

Claude Code espone un evento Stop che si attiva ogni volta che Claude si ferma — che sia perché ha finito, ha incontrato un errore o è in attesa di input dall'utente.

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

L'hook riceve su stdin un JSON contenente stop_reason (il motivo dello stop) e stop_message (l'ultimo messaggio di Claude). Puoi usare queste informazioni per personalizzare la notifica.

Struttura del JSON su stdin:

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

Opzione 1: notifiche desktop di sistema (la più semplice)

Su Linux si usa notify-send, su macOS osascript. Nessun servizio esterno richiesto:

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

Pro: zero configurazione, funziona subito.
Contro: la notifica arriva solo sulla macchina locale — se cambi dispositivo, non la vedi.


Opzione 2: Slack Webhook (consigliata per i team)

Usa un Incoming Webhook di Slack per inviare la notifica a un canale o in un messaggio diretto.

Prerequisiti:
1. Crea un'app nella pagina di gestione App di Slack
2. Attiva gli Incoming Webhooks e copia l'URL del webhook
3. Salva l'URL nella variabile d'ambiente 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

# Scegli l'emoji in base al motivo dello stop
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 &

Il & finale è importante: fa eseguire curl in background senza bloccare Claude.


Opzione 3: Bot Telegram

Un bot Telegram è ideale per uso personale — gratuito, istantaneo e le notifiche arrivano direttamente sullo smartphone.

Prerequisiti:
1. Crea un bot con @BotFather e ottieni il token
2. Recupera il tuo Chat ID (invia un messaggio al bot e chiama l'API getUpdates)
3. Imposta le variabili d'ambiente TELEGRAM_BOT_TOKEN e 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 &

Opzione 4: avviso sonoro (utile anche via SSH)

Se sei sulla stessa macchina ma hai cambiato finestra, un suono è spesso più efficace di un 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

Può essere combinato con le altre opzioni — suono locale più notifica push remota.


Configurazione completa: combinare più metodi

In pratica, conviene mettere tutto insieme in un unico 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"
          }
        ]
      }
    ]
  }
}

Qualche accortezza

Tutte le richieste di rete vanno in background

I curl nello script devono sempre avere & per andare in background. Se la rete è lenta o va in timeout, rischi di bloccare l'intera sessione di Claude. Aggiungere > /dev/null 2>&1 & è la prassi standard.

Stop non significa necessariamente "completato con successo"

Quando stop_reason è end_turn, di solito vuol dire che il task è finito, ma potrebbe anche indicare che Claude non sa come proseguire. Se vuoi ricevere notifiche solo a task effettivamente completato, puoi filtrare nello script:

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

Tronca il contenuto della notifica

L'ultimo messaggio di Claude può essere molto lungo (blocchi di codice, output corposi). Prima di inviarlo a Slack o Telegram, troncalo con head -c 300 per evitare notifiche chilometriche.

Tieni le variabili d'ambiente fuori dal repository

URL dei webhook e token dei bot non devono finire nel codice sorgente. Salvali nel tuo ~/.zshrc (o ~/.bashrc), oppure gestiscili con direnv.


Il risultato

Una volta configurato, il tuo modo di lavorare cambia:

  1. Assegni un task a Claude
  2. Passi a un'altra finestra o dispositivo
  3. Claude finisce → la notifica arriva
  4. Torni, controlli il risultato e vai avanti

Niente più attese davanti al terminale, niente più alt-tab compulsivi per controllare lo stato. Claude lavora, tu fai le tue cose, e quando ha finito te lo dice.