🪝

Claude CodeのHooksでSlackに会話内容を通知する

に公開

はじめに

株式会社アサインでプロダクトエンジニアとBizDevをしている坂井です。
Claude CodeのHooks機能を使いAIからの返答内容をSlackに自動通知する仕組みを作成したので紹介します。

こんな感じでslackの指定したチャンネルに投稿してくれます。

Claude Code の通知

Claude Code Hooksとは何かを知りたい方はこの記事が参考になるので、そちらをご覧ください。
簡単に言うと、Claude Codeの特定のアクション(応答完了時、Tool使用時、通知発生時)にシェルコマンドを実行することができる機能です。

https://dev.classmethod.jp/articles/claude-code-hooks-basic-usage/

とりあえず動かしたい方は前半に構築手順とコピペすると動くコードを置いているので、そちらをご覧ください。
仕組みまで知りたい方は後半に詳細な解説を記載しています。

背景

「Claude Codeからの返答に気づかず次の依頼が遅れる」「重めの処理を依頼している際にチラチラ見て気が散ってしまう」といったことがあったため、応答終了時に会話内容を含めてSlackに通知する仕組みを作成しました。
他の方が書かれている記事では応答終了時に「タスクが完了しました」と固定のメッセージをSlackに通知するものが多いですが、離席していても応答内容を確認したかったため、今回の記事ではAIの返答も含めてSlackに通知できるようにしています。

構築手順

以下の手順でClaude Codeからの通知フローを構築します。

  1. SlackのIncoming Webhook URLを取得
  2. 通知スクリプトを作成
  3. 通知スクリプトを環境変数に設定
  4. Claude CodeのHooks設定
  5. 実行

1. SlackのIncoming Webhook URLを取得

SlackのIncoming Webhook URLの取得方法はさまざまな方が紹介しているので、そちらを参考にしてください。

https://zenn.dev/shuuuuuun/articles/36a980f97c4c34

https://api.slack.com/messaging/webhooks

2. 通知スクリプトを作成

メインとなるシェルスクリプトを作成します。

今回は特定のプロジェクト限らず、全てのプロジェクトで応答終了時にSlackへ通知したいので、~/.claude/ 配下で作業します。
プロジェクト単位で設定したい場合はプロジェクトルートの .claude/ で作業すれば設定できます。適宜読み替えてお読みください。以降の作業は同様です。

まずは ~/.claude/ 配下に hooks/ ディレクトリを作成し、その中に notify_slack.sh という名前で以下のスクリプトを保存します。

#!/bin/bash

read -r input

# JSON入力から情報を抽出
transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')

# Slack Webhook URL
WEBHOOK_URL="${SLACK_WEBHOOK_URL}"

# トランスクリプトファイルから会話内容を抽出
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then

    # JSONLファイルからアシスタントのテキストメッセージのみ抽出
    MESSAGES=$(cat "$transcript_path" 2>/dev/null | \
        jq -r 'select(.type == "assistant" and .message.role == "assistant" and .message.content != null) | .message.content[] | select(.type == "text") | .text' 2>/dev/null | \
        awk '{printf "%s%s", (NR>1 ? "\n" : ""), $0}')

    if [ -n "$MESSAGES" ]; then
        # Slackに送信
        curl -X POST \
            -H "Content-type: application/json" \
            --data "{\"text\":\"🤖 $MESSAGES\"}" \
            "$WEBHOOK_URL" \
            --silent \
            --output /dev/null
    fi
fi

3. Slackの Webhook URLを環境変数に設定

シェルスクリプト内にSlackのWebhook URLをハードコーディングすると、GitHubに上げた際に漏洩してしまうため、環境変数として設定します。

お使いのシェルの設定ファイル(zshなら .zshrc、bashなら .bashrc)に以下を追加します。

# ~/.zshrcや~/.bashrcに追記
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"

"https://hooks.slack.com/..."の部分は、「1. SlackのIncoming Webhook URLを取得」で取得した Webhook URL に置き換えてください。

設定を反映させるには、ターミナルを再起動するか、以下のコマンドを実行します。

source ~/.zshrc  # zsh の場合
# または
source ~/.bashrc  # bash の場合

echo $SLACK_WEBHOOK_URL で設定した値が表示されるか確認してください。

4. Claude CodeのHooks設定

Claude Code側の設定です。
ここでは記事の冒頭でも話したように、全てのプロジェクトで共通の設定を行うため、~/.claude/settings.json に設定を追加します。

~/.claude/settings.json ファイルを開き(なければ作成し)、hooksセクションに以下のように追記します。

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/global/notify_slack.sh"
          }
        ]
      }
    ]
  }
}

5. 実行

これで準備完了です!では実際に試してみましょう。

  1. ターミナルを起動しClaude Codeを起動
  2. 何か適当に会話して、AIからの返答が返ってくる
  3. Slackの指定したチャンネルに通知が飛んでくる

無事に設定ができていれば以下のような通知が飛んでくるはずです。

Claude Code の通知

技術的な仕組み

ここからはスクリプトがどうやって動いているのか、実際のコードと照らし合わせながら詳しく解説していきます。

フックイベントの選択

1. イベントの種類

今回はAIからの応答終了時に通知させたいため「Stop」イベントを使用します。

フックイベントは徐々に増えてきており現在のバージョン(1.0.90)では以下のようなイベントがあります。

  • PreToolUse: ツール仕様前
  • PostToolUse: ツール使用後
  • Notification: 通知発生時(Claudeがツールを使用する許可が必要な場合など)
  • UserPromptSubmit: ユーザーのプロンプト送信時
  • Stop: メインエージェントの応答完了時 ←今回はこれを使用
  • SubagentStop: サブエージェントの応答完了時
  • SessionEnd: セッション終了時(clearコマンドやログアウトした時など)
  • PreCompact: コンパクト開始時
  • SessionStart: セッション開始時

公式ドキュメントにも記載があります。

https://docs.anthropic.com/en/docs/claude-code/hooks#hook-events

2. フックイベントの設定

次にStopイベントの設定を行います。
まずは公式ドキュメントにも記載されているフックの構造を確認します。

{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here"
          }
        ]
      }
    ]
  }
}
  • EventName: フックイベントの名前(今回は Stop
  • ToolPattern: 特定のツール使用時にフックを実行する場合のツール名(今回は空文字列)
  • command: 実行するコマンドのパス、またはコマンドを直接記述

構造が確認できたので、settings.jsonファイルに要件に合うようにStopイベントの設定を記述します。

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/global/notify_slack.sh"
          }
        ]
      }
    ]
  }
}

これでStopイベントが発生した際に、~/.claude/hooks/global/notify_slack.sh が実行されるようになります。

スクリプトの処理フロー

0. 標準入力の確認(デバッグ用)

Stopイベントが発生すると、Claude Codeはセッション情報を含むJSONを標準入力で渡します。
事前に標準入力の中身を確認するため、settings.jsonファイルを以下のように記述して出力します。

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "jq '.' >> ~/.claude/stop_hook_debug.log"
          }
        ]
      }
    ]
  }
}

こちらが実際に渡される標準入力のJSONです。

{
  "session_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "transcript_path": "/Users/<username>/.claude/projects/<escaped-project-path>/<session_id>.jsonl",
  "cwd": "/path/to/project",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}

今回は会話内容を取得したいため、transcript_path に格納されているパスを取得するできるようにします。

1. パスの抽出

まずは標準入力からJSONを読み込み、input 変数に格納します。

read -r input

次に、jqを使ってJSONから会話履歴ファイルのパスを抽出します。

transcript_path=$(echo "$input" | jq -r '.transcript_path // empty')
  • .transcript_path:JSONから transcript_path フィールドを取得
  • // empty: フィールドが存在しない場合は空文字を返す(エラー回避)

この処理により、transcript_path 変数には /tmp/claude_transcript_12345.jsonl のようなパスが格納されます。

3. 会話内容の解析

以下のワンライナーを使って会話履歴のJSONLから通知に必要な情報を抽出します。

MESSAGES=$(cat "$transcript_path" 2>/dev/null | \
    jq -r 'select(.type == "assistant" and .message.role == "assistant" and .message.content != null) | .message.content[] | select(.type == "text") | .text' 2>/dev/null | \
    awk '{printf "%s%s", (NR>1 ? "\n" : ""), $0}')

このワンライナーの処理内容は以下の通りです。

Step 3.1: トランスクリプトファイルの読み込み

cat "$transcript_path"

JSONL(JSON Lines)形式のファイルを読み込みます。各行が独立したJSONオブジェクトになっています。

{"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "こんにちは"}]}}
{"type": "assistant", "message": {"role": "assistant", "content": [{"type": "text", "text": "こんにちは!お手伝いできることはありますか?"}]}}
{"type": "assistant", "message": {"role": "assistant", "content": [{"type": "code", "language": "python", "text": "print('Hello')"}]}}

Step 3.2: jq によるフィルタリング

jq -r 'select(.type == "assistant" and .message.role == "assistant" and .message.content != null) |
       .message.content[] |
       select(.type == "text") |
       .text'

このコマンドは以下の処理を行っています。

  1. select(.type == "assistant" and ...): アシスタントの応答のみを選択
  2. .message.content[]: content 配列を展開
  3. select(.type == "text"): テキスト要素のみを選択(コードブロック等を除外)
  4. .text: テキストの内容を抽出

処理結果の例

こんにちは!お手伝いできることはありますか?
どのようなことでもお気軽にお尋ねください。

Step 3.3: awkによる整形

awk '{printf "%s%s", (NR>1 ? "\n" : ""), $0}'

複数行のテキストを適切に改行で連結します。

  • NR>1 ? "\n" : "": 2行目以降は改行を挿入
  • これにより複数の応答が自然な形で連結される

4. Slack への送信

最後に整形したメッセージをSlackに送信します。

if [ -n "$MESSAGES" ]; then
    curl -X POST \
        -H "Content-type: application/json" \
        --data "{\"text\":\"🤖 $MESSAGES\"}" \
        "$WEBHOOK_URL" \
        --silent \
        --output /dev/null
fi

SlackのIncoming Webhookには以下のJSONがHTTP POSTで送られ、チャンネルには text の内容だけがメッセージとして表示されます。

{
  "text": "🤖 こんにちは!お手伝いできることはありますか?\nどのようなことでもお気軽にお尋ねください。"
}

おわりに

今回はClaude CodeのHooks機能を使って、AIからの応答終了時に内容をSlackに通知する仕組みを作成しました。
実際に導入してみての気づきですが、完了タイミングがわかることのほかに、開始と終わりの間隔が明確になるため、命令に対してどの程度の時間で返答が返ってくるか徐々にわかってくるようになりました。
これが分かったからと言ってさほど大きな変化はないですが、並列させる開発以外のタスクをコントロールしたり、離席時間に応じて渡すタスクを変えたりと、なんとなくAIとの協力度が上がった気がします。

みなさんも、ぜひHooks機能便利なので色々と試してみてください!

ASSIGN

Discussion