Notificaciones automáticas con Hooks: que Claude te avise cuando termine

Configura un Stop Hook para que Claude te avise por escritorio, Slack o Telegram al terminar una tarea


Las tareas de Claude Code suelen tardar varios minutos o más. Le pides que refactorice un módulo, ejecute la suite de tests o procese un lote de archivos, y mientras tanto te vas a hacer otra cosa. El problema: no sabes cuándo terminó.

Cuando volvés a mirar, quizás ya terminó hace diez minutos. O quizás está trabado en un diálogo de confirmación esperándote.

Con el Hook Stop, podés recibir una notificación automática cada vez que Claude termina una tarea o necesita tu intervención. No importa en qué ventana o dispositivo estés: la notificación te llega directamente.


Cómo funciona

Claude Code tiene un evento Stop que se dispara cada vez que Claude se detiene, ya sea porque completó la tarea, encontró un error que necesita confirmación, o está esperando input del usuario.

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

El hook recibe por stdin un JSON con stop_reason (el motivo de la parada) y stop_message (lo último que dijo Claude). Con esa información podés personalizar el contenido de la notificación.

Estructura del JSON de entrada:

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

Opción 1: Notificación de escritorio (la más simple)

En Linux usás notify-send, en macOS osascript. No necesitás ningún servicio externo:

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

Ventaja: cero configuración, funciona de una.
Desventaja: solo la recibís en la máquina local. Si cambiás de dispositivo, no te enterás.


Opción 2: Slack Webhook (ideal para equipos)

Usá un Incoming Webhook de Slack para enviar notificaciones a un canal o mensaje directo.

Requisitos previos:
1. Crear una App en la página de administración de Slack
2. Activar Incoming Webhooks y copiar la URL del webhook
3. Guardar la URL en la variable de entorno 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 &

El & al final es clave: hace que curl se ejecute en segundo plano para no bloquear a Claude.


Opción 3: Bot de Telegram

Un Bot de Telegram es perfecto para uso personal: gratis, instantáneo, y te llega al celular.

Requisitos previos:
1. Crear un Bot con @BotFather y obtener el token
2. Obtener tu Chat ID (enviále un mensaje al Bot y consultá la API getUpdates)
3. Configurar las variables de entorno TELEGRAM_BOT_TOKEN y 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 &

Opción 4: Alerta sonora (útil también por SSH)

Si estás en la misma máquina pero cambiaste de ventana, un sonido puede ser más efectivo que 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

Podés combinar esto con las otras opciones: sonido local + notificación remota.


Configuración completa: combinar varios métodos

En la práctica, lo mejor es combinar varios métodos en un solo 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"
          }
        ]
      }
    ]
  }
}

Cosas a tener en cuenta

Todas las peticiones de red deben ejecutarse en segundo plano

Los curl del script de notificación siempre van con &. Si la red está lenta o hay un timeout, se bloquea toda la sesión de Claude. Agregar > /dev/null 2>&1 & es la práctica estándar.

Stop no significa "completado con éxito"

Que stop_reason sea end_turn generalmente indica que la tarea terminó, pero también puede ser que Claude no sabe qué hacer a continuación. Si solo querés notificaciones cuando realmente terminó, filtrá en el script:

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

Truncá el contenido de la notificación

El último mensaje de Claude puede ser muy largo (con bloques de código, salidas extensas). Antes de enviarlo a Slack o Telegram, usá head -c 300 para truncarlo y evitar mensajes interminables.

Las variables de entorno van en la configuración global

Las URLs de webhooks y los tokens de bots no deberían estar en el repositorio del proyecto. Lo ideal es ponerlos en ~/.zshrc (o ~/.bashrc), o manejarlos con direnv.


El resultado

Una vez configurado, tu forma de trabajar cambia:

  1. Le das una tarea a Claude
  2. Te vas a otra ventana o dispositivo
  3. Claude termina → te llega la notificación
  4. Volvés a ver los resultados y seguís con lo siguiente

No hace falta quedarse mirando la terminal ni ir y venir entre ventanas para ver si terminó. Claude trabaja, vos trabajás, y cuando termina te avisa.