Notificações automáticas com Hooks: saiba na hora quando o Claude terminar

Configure um Stop Hook para o Claude avisar automaticamente via desktop, Slack ou Telegram ao terminar uma tarefa


Tarefas no Claude Code costumam levar alguns minutos ou mais. Você pede pra ele refatorar um módulo, rodar a suite de testes, processar um lote de arquivos — e vai fazer outra coisa. O problema é que você não sabe quando ele terminou.

Quando você volta pra olhar, talvez já tenha acabado há dez minutos. Ou talvez esteja preso num prompt de confirmação esperando por você.

Com o Hook Stop, você recebe uma notificação automática sempre que o Claude terminar uma tarefa ou precisar da sua intervenção. Não importa em qual janela ou dispositivo você esteja — a notificação chega direto.


Como funciona

O Claude Code tem um evento Stop que é disparado toda vez que o Claude para, seja porque completou a tarefa, encontrou um erro que precisa de confirmação, ou está esperando input do usuário.

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

O hook recebe via stdin um JSON contendo stop_reason (o motivo da parada) e stop_message (a última coisa que o Claude disse). Com essas informações, você pode personalizar o conteúdo da notificação.

Estrutura do JSON de entrada:

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

Opção 1: Notificação do sistema (a mais simples)

No Linux usa notify-send, no macOS usa osascript. Não precisa de nenhum serviço 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

Vantagem: zero configuração, funciona de cara.
Desvantagem: só funciona na máquina local. Se você trocar de dispositivo, não vai ver.


Opção 2: Slack Webhook (recomendado para times)

Use um Incoming Webhook do Slack para enviar notificações para um canal ou mensagem direta.

Pré-requisitos:
1. Crie um App na página de gerenciamento do Slack
2. Ative Incoming Webhooks e copie a URL do webhook
3. Salve a URL na variável de 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

# 根据停止原因选 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 &

O & no final é essencial — faz o curl rodar em segundo plano sem bloquear o Claude.


Opção 3: Bot do Telegram

Um Bot do Telegram é perfeito para uso pessoal: gratuito, instantâneo, e a notificação chega direto no celular.

Pré-requisitos:
1. Crie um Bot com o @BotFather e pegue o token
2. Descubra seu Chat ID (mande uma mensagem pro Bot e consulte a API getUpdates)
3. Configure as variáveis de 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 &

Opção 4: Alerta sonoro (funciona bem até por SSH)

Se você está na mesma máquina mas trocou de janela, um som pode ser mais eficiente que um 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

Dá pra combinar com as outras opções — som local + notificação remota.


Configuração completa: combinando vários métodos

Na prática, o ideal é combinar vários métodos num único 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"
          }
        ]
      }
    ]
  }
}

Pontos de atenção

Todas as requisições de rede devem rodar em segundo plano

Os curl no script de notificação sempre precisam do &. Se a rede estiver lenta ou der timeout, trava a sessão inteira do Claude. Usar > /dev/null 2>&1 & é o padrão.

Stop não significa "concluído com sucesso"

Um stop_reason igual a end_turn geralmente indica que a tarefa terminou, mas também pode significar que o Claude não sabe o que fazer em seguida. Se você só quer ser notificado quando realmente terminou, filtre no script:

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

Truncar o conteúdo da notificação

A última mensagem do Claude pode ser bem longa (com blocos de código, saídas extensas). Antes de mandar pro Slack ou Telegram, use head -c 300 pra truncar e evitar mensagens gigantes.

Variáveis de ambiente devem ficar na configuração global

URLs de webhooks e tokens de bots não devem ficar no repositório do projeto. O ideal é colocar no ~/.zshrc (ou ~/.bashrc), ou gerenciar com direnv.


O resultado

Depois de configurar, seu jeito de trabalhar muda:

  1. Você dá uma tarefa pro Claude
  2. Vai pra outra janela ou dispositivo
  3. Claude termina → a notificação chega
  4. Você volta, confere o resultado e segue pro próximo passo

Sem ficar olhando pro terminal, sem ficar alternando entre janelas pra ver se acabou. O Claude trabalha, você trabalha, e quando ele terminar, te avisa.