Hooks로 작업 완료 알림 보내기: Claude가 끝나면 알아서 알려준다

Stop Hook을 설정해서 Claude가 작업을 마치면 데스크톱 알림, Slack, Telegram으로 자동 알림


Claude Code로 작업을 실행하면 몇 분, 길면 그 이상 걸리는 경우가 많다. 모듈 리팩토링, 테스트 스위트 실행, 파일 일괄 처리 같은 걸 맡겨놓고 다른 창으로 넘어가서 할 일을 한다. 문제는 언제 끝났는지 알 수가 없다는 것이다.

돌아와 보면 10분 전에 이미 끝나 있거나, 확인 대화상자에서 멈춘 채 입력을 기다리고 있거나.

Stop Hook을 쓰면 Claude가 작업을 마쳤을 때(또는 사용자 개입이 필요할 때) 자동으로 알림을 보낼 수 있다. 어느 창에 있든, 어느 기기에 있든 알림이 바로 날아온다.


핵심 원리

Claude Code에는 Stop 이벤트가 있다. Claude가 멈출 때마다 발생하는 이벤트로, 작업 완료·에러로 인한 확인 대기·사용자 입력 대기 등 모든 경우에 트리거된다.

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

Hook은 stdin으로 JSON을 받는다. stop_reason(멈춘 이유)과 stop_message(Claude의 마지막 메시지)가 포함되어 있어서, 이걸로 알림 내용을 원하는 대로 구성할 수 있다.

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으로 지정한 채널이나 DM에 알림을 보낸다.

사전 준비:
1. Slack App 관리 페이지에서 App 생성
2. Incoming Webhooks를 활성화하고 Webhook URL 확보
3. 환경 변수 SLACK_WEBHOOK_URL에 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 Bot

Telegram Bot은 개인 용도로 쓰기 좋다. 무료이고 실시간이며, 스마트폰에서 바로 받을 수 있다.

사전 준비:
1. @BotFather에서 Bot을 만들고 Token 확보
2. 본인의 Chat ID 확인 (Bot에 메시지를 보낸 뒤 getUpdates API 호출)
3. 환경 변수 TELEGRAM_BOT_TOKENTELEGRAM_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_reasonend_turn이면 보통 작업 완료를 뜻하지만, Claude가 다음에 뭘 해야 할지 판단하지 못했을 때도 반환될 수 있다. 정말로 완료됐을 때만 알림을 받고 싶으면 스크립트에서 필터링하면 된다:

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

알림 내용은 잘라서 보낼 것

Claude의 마지막 메시지는 길어질 수 있다(코드 블록이나 대량 출력이 포함되는 경우). Slack이나 Telegram으로 보내기 전에 head -c 300으로 잘라서 메시지 폭주를 방지하자.

환경 변수는 글로벌 설정에 저장할 것

Webhook URL이나 Bot Token을 프로젝트 저장소에 커밋하면 안 된다. ~/.zshrc(또는 ~/.bashrc)에 넣거나 direnv로 관리하는 걸 추천한다.


적용 후 달라지는 점

설정을 마치면 작업 흐름이 바뀐다:

  1. Claude에 작업을 맡긴다
  2. 다른 창이나 기기로 전환한다
  3. Claude가 완료하면 → 알림이 온다
  4. 돌아와서 결과를 확인하고 다음 단계로 넘어간다

터미널을 멍하니 지켜볼 필요가 없다. 창을 계속 왔다 갔다 할 필요도 없다. Claude는 Claude 일을 하고, 나는 내 일을 한다. 끝나면 알려준다.