WSL2環境Claude Code の入力待ちを Windows トースト通知で気付けるようにする (BurntToast + hook)

WSL2環境Claude Code の入力待ちを Windows トースト通知で気付けるようにする (BurntToast + hook)

2026.05.21

はじめに

データ事業本部の荒木です。

Claude Code を複数リポジトリ並行で動かしていると、AskUserQuestion や許可プロンプトでユーザー入力待ちになった瞬間に気付けないことはありました。
別のタブで作業しているとそのまま処理が停止していた、ということが頻発したので、デスクトップ通知を出す仕組みを整えました。

Claude Code には標準で preferredNotifChannel 設定キーがあり、公式ドキュメントには次のように書かれています。

preferredNotifChannel
Method for task-complete and permission-prompt notifications: "auto", "terminal_bell", "iterm2", "iterm2_with_bell", "kitty", "ghostty", or "notifications_disabled". Default: "auto", which sends a desktop notification in iTerm2, Ghostty, and Kitty and does nothing in other terminals.

(Claude Code settings reference)

By default Claude Code sends a desktop notification only in Ghostty, Kitty, and iTerm2. In other terminals, set preferredNotifChannel to "terminal_bell" to ring the terminal bell instead, or configure a Notification hook for a custom sound or command.

(Configure your terminal for Claude Code)

つまり auto (デフォルト) は Ghostty / Kitty / iTerm2 のみ対応で、それ以外の端末では何もしません。WSL2 + Windows Terminal は「それ以外」に該当するため、デフォルトのままだとデスクトップ通知が出ません。

WSL2 + Windows Terminal で Claude Code のデスクトップ通知を出す方法として、PowerShell + BurntToast モジュール + Notification hook + マーカー方式の組み合わせを無理やり実装したのでご紹介します。

環境

  • OS: Windows 11 + WSL2 (Ubuntu)、WSLg 有効
  • ターミナル: Windows Terminal
  • Claude Code: WSL2 内で稼働
  • PowerShell: 5.1 (Windows 標準)
  • BurntToast: 0.x (PowerShell Gallery から取得)

本題

採用する構成

最終的な構成は次の通りです。

コンポーネント 役割
BurntToast (PowerShell モジュール) Windows のトースト通知を発行する
notify-claude-input.sh (Bash) hook から呼ばれる本体。BurntToast を起動 + paplay で音 + マーカー方式の遅延通知
~/.claude/settings.jsonNotification hook AskUserQuestion 等の入力待ちで発火 (30 秒遅延付き)
~/.claude/settings.jsonPostToolUse hook ツール実行完了時にマーカーを掃除 (ユーザーが反応して処理が再開したタイミング)

BurntToast のインストール

PowerShell 経由でモジュールを入れます。NuGet provider を先に入れてから BurntToast を取得します。

# NuGet provider を先にインストール (これがないと BurntToast のインストールで止まる)
powershell.exe -NoProfile -Command "Install-PackageProvider -Name NuGet -Scope CurrentUser -Force"

# BurntToast 本体をインストール
powershell.exe -NoProfile -Command "Install-Module -Name BurntToast -Scope CurrentUser -Force"

PowerShell には ExecutionPolicyというローカルでスクリプトを実行できるかどうかを制御するセキュリティ設定がありエラーになってしまうため、インストール後-ExecutionPolicy Bypass を都度指定して恒久的な ExecutionPolicy 変更なしでモジュールが読めるようなコマンドを実行します

powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "New-BurntToastNotification -Text 'Claude Code', '入力待ちです'"

スクリーンショット 2026-05-21 194800

上記コマンドをPowerShellで実行して、Windows の通知センターにトーストが表示されれば、BurntToast 単独動作は完了です。

通知スクリプトの作成

hook から呼ばれる本体スクリプトを ~/claude-work/scripts/notify-claude-input.sh として作成します。

#!/bin/bash
# Claude Code Notification hook: WSL2 → Windows トースト通知 + 完了音
#
# 使い方:
#   bash notify-claude-input.sh [MESSAGE [DELAY_SEC]]   # 通知発火 (即時 or 遅延 ARM)
#   bash notify-claude-input.sh --clear                  # 自セッションのマーカーをクリア

set +e

# hook 経由なら stdin に JSON で cwd / session_id 等が来る
HOOK_JSON=""
if [ ! -t 0 ]; then
    HOOK_JSON=$(cat 2>/dev/null)
fi

# JSON から session_id と cwd を抽出
SESSION_ID=""
PROJECT_DIR=""
if [ -n "$HOOK_JSON" ] && command -v python3 >/dev/null 2>&1; then
    SESSION_ID=$(printf '%s' "$HOOK_JSON" | python3 -c 'import sys,json
try:
    print(json.load(sys.stdin).get("session_id",""))
except Exception:
    pass' 2>/dev/null)
    PROJECT_DIR=$(printf '%s' "$HOOK_JSON" | python3 -c 'import sys,json
try:
    print(json.load(sys.stdin).get("cwd",""))
except Exception:
    pass' 2>/dev/null)
fi

PROJECT_DIR="${PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-$PWD}}"
REPO_NAME=$(basename "$PROJECT_DIR")
SESSION_ID="${SESSION_ID:-unknown}"
SHORT_SID="${SESSION_ID:0:8}"

LOG="/tmp/notify-claude-input.log"
MARKER_BASE="/tmp/claude-input-pending-${SESSION_ID}"

# --clear モード: 自セッションのマーカーだけ削除
if [ "$1" = "--clear" ]; then
    COUNT=$(ls "${MARKER_BASE}-"* 2>/dev/null | wc -l)
    echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') CLEAR session=${SHORT_SID} markers=${COUNT}" >> "$LOG"
    rm -f "${MARKER_BASE}-"*
    exit 0
fi

# 通常モード: 通知発火 (ARM or 即時)
TITLE="Claude Code: ${REPO_NAME}"
MESSAGE="${1:-入力待ちです}"
DELAY="${2:-0}"
SOUND="/usr/share/sounds/freedesktop/stereo/complete.oga"
MARKER="${MARKER_BASE}-$$"

do_notify() {
    [ -f "$SOUND" ] && paplay "$SOUND" >/dev/null 2>&1 &
    if command -v powershell.exe >/dev/null 2>&1; then
        local PS_CMD="New-BurntToastNotification -Text '${TITLE}', '${MESSAGE}'"
        local ENCODED
        ENCODED=$(printf '%s' "$PS_CMD" | iconv -t UTF-16LE 2>/dev/null | base64 -w 0)
        [ -n "$ENCODED" ] && powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand "$ENCODED" >/dev/null 2>&1 &
    fi
}

if [ "$DELAY" -gt 0 ] 2>/dev/null; then
    # 遅延モード: マーカー作成 → bg で sleep 後にマーカー残存チェック
    touch "$MARKER"
    echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') pid=$$ session=${SHORT_SID} ARM delay=${DELAY}s repo=${REPO_NAME} msg=${MESSAGE}" >> "$LOG"
    (
        sleep "$DELAY"
        if [ -f "$MARKER" ]; then
            rm -f "$MARKER"
            echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') pid=$$ session=${SHORT_SID} FIRE (no response in ${DELAY}s)" >> "$LOG"
            do_notify
        else
            echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') pid=$$ session=${SHORT_SID} SKIP (user responded within ${DELAY}s)" >> "$LOG"
        fi
    ) &
    disown 2>/dev/null
else
    # 即時モード
    echo "$(date '+%Y-%m-%d %H:%M:%S.%3N') pid=$$ session=${SHORT_SID} FIRE immediate repo=${REPO_NAME} msg=${MESSAGE}" >> "$LOG"
    do_notify
fi

exit 0

ポイント:

  • マーカーをセッション ID + PID で隔離: 複数セッションを並行運用しても、別セッションが --clear でマーカーを削除しても自セッション分は影響を受けません
  • --clear モード: stdin の JSON から session_id を取り出して、自セッションのマーカーだけ一括削除します。PostToolUse hook からこのモードで呼び出します
  • リポジトリ名の自動付与: cwdbasename を通知タイトルに組み込んでいるので、どのプロジェクトからの通知か即判別できます
  • PowerShell 5.1 日本語化け対策: -Command 直接渡しは Shift-JIS で化けるので、iconv -t UTF-16LE | base64 経由で -EncodedCommand に渡しています

実行権限を付けておきます。

chmod +x ~/claude-work/scripts/notify-claude-input.sh

Claude Code の hook 設定

~/.claude/settings.json に hook を 2 種類登録します。

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          { "type": "command", "command": "bash /home/<USER>/claude-work/scripts/notify-claude-input.sh '入力待ちです' 30" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          { "type": "command", "command": "bash /home/<USER>/claude-work/scripts/notify-claude-input.sh --clear" }
        ]
      }
    ]
  }
}

ポイントは次の 2 点です。

  • Notification hookAskUserQuestion/hooks ダイアログ表示、その他「Claude がアイドル状態」全般で発火します。ターン終了時 (Claude が応答を出し終えて次のユーザー入力を待つ状態) でも発火するので、即時通知にすると煩いです。30 秒遅延を入れて誤通知を抑えます
  • PostToolUse hook はツール実行が完了したタイミングで発火します。ここで自セッションのマーカーを --clear で削除することで、「ユーザーが操作を再開した = 通知不要」を表現します

動作確認

hook 構成後、実際に許可プロンプトを発火させて Windows トーストが出るか確認します。

スクリーンショット 2026-05-21 195140

Windows の通知センターにトーストが表示されました。通知タイトルにはリポジトリ名が入っているので、複数セッション並行運用でもどのプロジェクトからの通知か即判別できます。

余分な通知を抑える遅延モードの仕組み

Notification hook に 30 を渡しているのは、ターン終了直後の不要な通知を抑えるためです。

公式ドキュメントによると、Notification hook の発火条件は次の通りです。

When Claude finishes a task or pauses for a permission prompt, it fires a notification event.

(Configure your terminal for Claude Code)

「タスク完了時 = 次のユーザー入力を待つ状態 (idle) に入った瞬間」も含まれるため、AskUserQuestion を呼んでいなくてもターンが終わった直後に毎回発火します。これを即時通知にしてしまうと、画面を見ながら連続で操作しているときも鳴って煩い、ということが起こります。

遅延モードの動きは以下の通りです。

  1. Notification hook 発火 → セッション ID + PID 付きのマーカーファイルを作成 + バックグラウンドで sleep 30 を仕掛ける
  2. 30 秒以内にユーザーが操作を再開 → PostToolUse hook 発火 → --clear で自セッションのマーカーを削除 → 通知は SKIP
  3. 30 秒経過してもマーカーが残っている (= ユーザーが画面を見ていない) → 通知発火

マーカーをセッション ID で隔離しているのは、複数の Claude Code セッションを並行運用したときに、別セッションの PostToolUse が自セッションのマーカーを巻き込んで消してしまわないようにするためです。これがないと、片方のセッションで作業しているともう片方の通知が出ない、という挙動になります。

遅延秒数は呼び出し側 (settings.json の hook command の第 2 引数) で変更できるので、30 秒以外にしたい場合はそこを書き換えるだけでOKです。

まとめ

WSL2 + Windows Terminal で Claude Code のデスクトップ通知を出す方法として、PowerShell から Windows トーストを呼び出す構成を実際に組んで試してみました。
デフォルトでは通知が届かない環境でしたが、入力待ちや許可待ちのタイミングで気付けるようになり、別タブで作業しているときの待ち時間がかなり減りました。

WSL2 + Claude Code 環境で「入力待ちに気付けない」と悩んでいる方の参考になれば幸いです。

参照


生成AI活用はクラスメソッドにお任せ

過去に支援してきた生成AIの支援実績100+を元にホワイトペーパーを作成しました。御社が抱えている課題のうち、どれが解決できて、どのようなサービスが受けられるのか?4つのフェーズに分けてまとめています。どうぞお気軽にご覧ください。

生成AI資料イメージ

無料でダウンロードする

この記事をシェアする

関連記事