
- 背景
- なぜ alerter を採用したのか
- 1. alerter のインストール
- 2. 通知スクリプトの作成
- 3. Claude Code の hooks 設定
- 4. VSCode 拡張での Notification hook の扱い
- 5. macOS のセキュリティ許可
- 6. 動作確認
- 7. alerter のプロセス管理で学んだこと
- 8. なぜ nohup + disown が必要だったか
- 9. 通知のカスタマイズ
- まとめ
- 最後に
こんにちは、開発本部 開発2部 RetailHUB NetSuperグループに所属するホーク🦅アイ👁️です。
背景
弊社ではClaude を非エンジニアも含めた全社に展開しており、業務のあらゆる場面で生成AI の活用を推進しています。
そんな中、我々のチーム内でも今年3月から本格的にCursor から移行してClaude Code (VSCode 拡張機能)を日常的に使うようになってから、両者の明らかな違いを実感することになりました。
それは、Cursor が標準搭載しているmacOS デスクトップ通知機能でした。Claude Code にはその機能がないためAgent にプロンプトを投げた後、私自身が他の作業を並行しているとClaude Code 側が permission_prompt のWait でタスクが一向に完了できない状態やタスク完了状態に気付くのが随分遅れてしまうということがしばしばありました(業務効率化のためのAgent ツールなのに、、)。
Claude Code には Hooks という仕組みが用意されています。これは Stop(応答終了)や Notification(許可待ち等)、PreToolUse(ツール実行直前)といったライフサイクルイベントに対して任意のシェルコマンドを実行できる公式機能で、JSON がイベント情報として標準入力から渡ってきます。
本記事ではこの Hooks と alerter というコマンドラインツールを組み合わせて、
- タスク完了・許可待ち・入力待ちのデスクトップ通知を出す
- 通知をクリックすると、対象プロジェクトの VSCode ウィンドウが自動でアクティブになる(全画面の別アプリ上からでも切り替わる)
- VSCode 拡張版でも許可待ち通知を取りこぼさない
という環境を構築した内容をまとめます。macOS 26 系(Tahoe)環境で動作確認しています。
なぜ alerter を採用したのか
macOS から通知を出すだけなら選択肢は複数あります。今回の要件「通知をクリックしたら VSCode がアクティブになる」を満たせるものを比較した結果を表にまとめます。
| ツール | 通知表示 | クリックイベントの取得 | 備考 |
|---|---|---|---|
terminal-notifier |
環境依存 | 可能(旧来の定番) | 公式リポジトリの最新リリースは 2017 年 11 月(v2.0.0)で、近年の macOS での動作不具合 Issue(#307、#312、#319 ほか)が未解決のままです。私の環境(macOS 26 系)では通知が出ませんでした。 |
osascript(display notification) |
動作する | 不可 | AppleScript 公式ドキュメント(Standard Additions: display notification)に「戻り値なし」と明記されており、クリック結果を取得する手段がありません。 |
alerter |
動作する | 可能 | 公式リポジトリによれば、terminal-notifier を Swift で書き直した後継で、macOS 13.0 以降対応。クリック時に @CONTENTCLICKED / @ACTIONCLICKED を stdout に出力するため、外部プロセスでの後処理が可能です。 |
alerter がクリック結果を stdout に返してくれるおかげで、「クリック → open -a "Visual Studio Code" で対象プロジェクトを開く」という連携を、標準ツールの組み合わせだけで実現できました。
1. alerter のインストール
Homebrew で導入します(公式の導入手順に準拠)。
brew install vjeantet/tap/alerter
インストール確認:
which alerter # /opt/homebrew/bin/alerter alerter --version
2. 通知スクリプトの作成
2 つのスクリプトを ~/.claude/ に配置し、実行権限を付与します。前者は Stop / Notification hook 用、後者は VSCode 拡張向けの PreToolUse hook 用です。
chmod +x ~/.claude/notify_alerter.sh chmod +x ~/.claude/notify_pretool.sh
2-1. notify_alerter.sh(Stop / Notification hook 用)
タスク完了通知および、CLI 版 Claude Code での許可待ち通知を処理します。Hook に渡ってくる JSON の仕様は 公式リファレンスの Stop / Notification セクションに従っています。notification_type として permission_prompt / idle_prompt が返ってくるため、これで分岐しています。
#!/bin/bash input=$(cat) echo "$(date '+%H:%M:%S') $input" >> /tmp/claude_notify_debug.log cwd=$(echo "$input" | jq -r '.cwd') project=$(basename "$cwd") notification_type=$(echo "$input" | jq -r '.notification_type') # ターミナルアプリの Bundle ID を自動検出 get_terminal_bundle_id() { if [[ -n "${__CFBundleIdentifier}" ]]; then echo "${__CFBundleIdentifier}" return fi case "${TERM_PROGRAM}" in "Apple_Terminal") echo "com.apple.Terminal" ;; "iTerm.app") echo "com.googlecode.iterm2" ;; "ghostty") echo "com.mitchellh.ghostty" ;; "WarpTerminal") echo "dev.warp.Warp-Stable" ;; *) local pid parent comm pid=$$ while [[ "${pid}" -ne 1 ]] 2>/dev/null; do parent=$(ps -p "${pid}" -o ppid= 2>/dev/null | tr -d ' ') || break [[ -z "${parent}" ]] && break comm=$(ps -p "${parent}" -o comm= 2>/dev/null) case "${comm}" in *Terminal*) echo "com.apple.Terminal"; return ;; *iTerm*) echo "com.googlecode.iterm2"; return ;; *Cursor*) echo "com.todesktop.230313mzl4w4u92"; return ;; *Code*) echo "com.microsoft.VSCode"; return ;; *ghostty*) echo "com.mitchellh.ghostty"; return ;; *warp*) echo "dev.warp.Warp-Stable"; return ;; *) ;; esac pid="${parent}" done echo "" ;; esac } BUNDLE_ID=$(get_terminal_bundle_id) send_notification() { local message="$1" local sound="$2" local group="$3" local args=(--title "Claude Code" --subtitle "${project}" --message "${message}") if [[ -n "${sound}" ]]; then args+=(--sound "${sound}") fi args+=(--sender "com.microsoft.VSCode") # --group: 同じグループの通知は前のプロセスを自動終了して置き換える args+=(--group "${group:-claude-default}") # --timeout: プロセスのゾンビ化防止(秒)。通知自体は macOS 通知センターに残る local timeout=86400 local timeout_file="$HOME/.claude/notify_timeout.conf" if [[ -f "${timeout_file}" ]]; then timeout=$(cat "${timeout_file}" | tr -d '[:space:]') fi args+=(--timeout "${timeout}") # alerter はクリック待ちでブロックするため、nohup + disown で完全にデタッチ nohup bash -c " result=\$(alerter $(printf '%q ' "${args[@]}") 2>/dev/null) if [[ \"\${result}\" == \"@CONTENTCLICKED\" || \"\${result}\" == \"@ACTIONCLICKED\" ]] && [[ -n \"${cwd}\" ]]; then open -a \"Visual Studio Code\" \"${cwd}\" fi " &>/dev/null & disown } case "${notification_type}" in "permission_prompt") send_notification "許可待ち" "Ping" "claude-permission" ;; "idle_prompt") send_notification "入力待ち" "Purr" "claude-idle" ;; "stop") send_notification "タスク完了" "Glass" "claude-stop" ;; *) send_notification "通知" "default" "claude-other" ;; esac
2-2. notify_pretool.sh(PreToolUse hook 用)
こちらは VSCode 拡張環境向けの「許可待ち通知」の代替実装です。詳細は「4. VSCode 拡張での Notification hook の扱い」で後述します。
ざっくり説明すると、次の 4 つの設定ファイルの permissions.allow リストと照合し、自動許可されないツールの実行前にのみ通知を送るというロジックです。
~/.claude/settings.json(グローバル)~/.claude/settings.local.json(グローバルローカル)$cwd/.claude/settings.json(プロジェクト)$cwd/.claude/settings.local.json(プロジェクトローカル)
#!/bin/bash # PreToolUse hook: 許可が必要なツール実行前に通知を送る # settings.json の allow リストにマッチするツールはスキップする input=$(cat) tool_name=$(echo "$input" | jq -r '.tool_name') cwd=$(echo "$input" | jq -r '.cwd') project=$(basename "$cwd") # 常に自動許可されるツール(通知不要) case "${tool_name}" in Glob|Grep|TodoWrite|Agent|Skill|ToolSearch|SendMessage) exit 0 ;; esac # ユーザー個別のスキップリスト(~/.claude/notify_skip_tools.txt) SKIP_FILE="$HOME/.claude/notify_skip_tools.txt" if [[ -f "${SKIP_FILE}" ]]; then while IFS= read -r skip_tool; do [[ -z "${skip_tool}" || "${skip_tool}" == \#* ]] && continue if [[ "${tool_name}" == "${skip_tool}" ]]; then exit 0 fi done < "${SKIP_FILE}" fi # allow リストと照合する関数 check_allow_list() { local settings_file="$1" [[ -f "${settings_file}" ]] || return # Bash ツール: コマンドプレフィックスで照合 if [[ "${tool_name}" == "Bash" ]]; then local command command=$(echo "$input" | jq -r '.tool_input.command') while IFS= read -r pattern; do if [[ "${pattern}" =~ ^Bash\((.+)(:\*|\*)?\)$ ]]; then local prefix="${BASH_REMATCH[1]}" prefix="${prefix%:*}" if [[ "${command}" == "${prefix}"* ]]; then exit 0 fi fi done < <(jq -r '.permissions.allow[]' "${settings_file}" 2>/dev/null) fi # Read ツール: パスパターンで照合 if [[ "${tool_name}" == "Read" ]]; then local file_path file_path=$(echo "$input" | jq -r '.tool_input.file_path') while IFS= read -r pattern; do if [[ "${pattern}" =~ ^Read\(//(.+)\)$ ]]; then local path_pattern="${BASH_REMATCH[1]}" local path_prefix="${path_pattern%%/**}" if [[ "${file_path}" == "${path_prefix}"* ]]; then exit 0 fi fi done < <(jq -r '.permissions.allow[]' "${settings_file}" 2>/dev/null) fi # MCP ツール・WebSearch 等: 完全一致で照合 while IFS= read -r pattern; do if [[ "${pattern}" == "${tool_name}" ]]; then exit 0 fi done < <(jq -r '.permissions.allow[]' "${settings_file}" 2>/dev/null) } # グローバル設定 check_allow_list "$HOME/.claude/settings.json" check_allow_list "$HOME/.claude/settings.local.json" # プロジェクト設定 check_allow_list "$cwd/.claude/settings.json" check_allow_list "$cwd/.claude/settings.local.json" # 許可リストにマッチしない → 通知を送る echo "$(date '+%H:%M:%S') PRETOOL_NOTIFY: ${tool_name}" >> /tmp/claude_notify_debug.log nohup bash -c " timeout=86400 timeout_file=\"\$HOME/.claude/notify_timeout.conf\" if [[ -f \"\${timeout_file}\" ]]; then timeout=\$(cat \"\${timeout_file}\" | tr -d '[:space:]') fi result=\$(alerter --title 'Claude Code' --subtitle '${project}' --message '許可待ち: ${tool_name}' --sound Ping --sender com.microsoft.VSCode --group claude-pretool --timeout \"\${timeout}\" 2>/dev/null) if [[ \"\${result}\" == '@CONTENTCLICKED' || \"\${result}\" == '@ACTIONCLICKED' ]] && [[ -n '${cwd}' ]]; then open -a 'Visual Studio Code' '${cwd}' fi " &>/dev/null & disown exit 0
3. Claude Code の hooks 設定
~/.claude/settings.json の hooks セクションに以下を追加します(公式リファレンスの書式に準拠)。
{ "hooks": { "Stop": [ { "matcher": "", "hooks": [ { "type": "command", "command": "echo '{\"cwd\": \"'\"$(pwd)\"'\", \"notification_type\": \"stop\"}' | ~/.claude/notify_alerter.sh" } ] } ], "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "~/.claude/notify_alerter.sh" } ] } ], "PreToolUse": [ { "matcher": "", "hooks": [ { "type": "command", "command": "~/.claude/notify_pretool.sh" } ] } ] } }
各 Hook の役割
| Hook | 発火タイミング | 用途 | VSCode 拡張 | CLI |
|---|---|---|---|---|
Stop |
Claude が応答を終えて停止したタイミング | 「タスク完了」通知 | 動作する | 動作する |
Notification |
許可待ち・入力待ちなどの通知イベント | 「許可待ち」「入力待ち」通知 | permission_prompt が発火しないケースあり |
動作する |
PreToolUse |
ツール実行の直前 | VSCode での「許可待ち」通知の代替 | 動作する | 動作する |
4. VSCode 拡張での Notification hook の扱い
公式リファレンスでは、Notification hook の notification_type として permission_prompt / idle_prompt / auth_success / elicitation_dialog の 4 種が定義されています。しかし、私の環境で動作確認したところ、VSCode 拡張版では許可ダイアログが出ても Notification hook(permission_prompt)が発火しないケースがあり、「許可待ちなのに通知が来ない」という状態になっていました。CLI 版では同じ設定で期待どおり発火しています。
そのため、VSCode 拡張で使う場合は PreToolUse hook(必ず発火する)でツール実行直前に自前で判定するという回避策を取っています。流れは以下です。
PreToolUsehook がツール実行直前に発火するnotify_pretool.shがツール名(と Bash の場合はコマンド、Read の場合はファイルパス)を受け取り、4 つの設定ファイルのpermissions.allowと照合する- allow リストにマッチしなかったときだけ通知を送る(=「このあと許可ダイアログが出るはず」というタイミング)
この方式であれば、Notification hook の発火有無にかかわらず、VSCode でも CLI でも漏れなく許可待ち通知を届けられます。CLI 版では Notification hook が正常動作するため、重複しないよう --group を claude-permission と claude-pretool で分けています(後述)。
5. macOS のセキュリティ許可
alerter + open -a の組み合わせは、macOS のアクセシビリティ・オートメーション等の追加許可なしで動作しました。初回のみ通知センター側で通知の表示許可を求められる程度で、特別な設定は不要です。
6. 動作確認
通知テスト
# タスク完了通知 echo '{"cwd": "'$(pwd)'", "notification_type": "stop"}' | ~/.claude/notify_alerter.sh # 許可待ち通知(CLI の Notification hook 用) echo '{"cwd": "'$(pwd)'", "notification_type": "permission_prompt"}' | ~/.claude/notify_alerter.sh
確認項目
- タスク完了通知がデスクトップに表示される
- 許可待ち通知が表示される(VSCode: PreToolUse / CLI: Notification)
- VSCode アイコンが通知に表示される(
--sender com.microsoft.VSCode) - 通知をクリックすると対象プロジェクトの VSCode ウィンドウがアクティブになる
- 全画面の別アプリ(Chrome 等)から通知をクリックしても正しいウィンドウに切り替わる
- 通知後に Claude が WAIT 状態にならず即座に続行する
デバッグログ
通知が来ないときはデバッグログを確認します:
tail -f /tmp/claude_notify_debug.log
7. alerter のプロセス管理で学んだこと
運用してみて一番ハマったのがプロセス管理です。
問題: プロセスのゾンビ化
alerter はクリックされるまで stdout をブロックし続ける仕様です(公式リポジトリの README にある @CONTENTCLICKED / @ACTIONCLICKED / @TIMEOUT / @CLOSED のいずれかが出力されるまでプロセスが生きる)。通知バッジを macOS 通知センターから消去しても alerter プロセスは終了しません。放置すると各プロセスがメモリを消費し、長時間の利用で数 GB に達するケースがありました。
対策1: --group(プロセス蓄積の防止)
同じ --group の通知が新たに発行されると、前のプロセスが自動で kill されます。グループは用途別に分けており、同時に存在するプロセスは最大 4 つになる設計です:
| グループ | 用途 |
|---|---|
claude-stop |
タスク完了 |
claude-permission |
許可待ち(CLI Notification hook) |
claude-pretool |
許可待ち(VSCode PreToolUse hook) |
claude-idle |
入力待ち |
対策2: --timeout(最終的なプロセス回収)
--group だけでは最後の 4 プロセスが残り続けるため、--timeout でプロセスの最大生存時間を設定して確実に回収します。
- デフォルト: 86400 秒(1 日)
- カスタム:
~/.claude/notify_timeout.confに秒数を書く
# 例: 2 時間に変更 echo 7200 > ~/.claude/notify_timeout.conf
なお、timeout が切れてもプロセスが終了するだけで、macOS 通知センターの通知バッジは残ります。
溜まったプロセスの手動クリーンアップ
# alerter プロセス数を確認 ps aux | grep alerter | grep -v grep | wc -l # 全 alerter プロセスを終了 pkill -f alerter
8. なぜ nohup + disown が必要だったか
前述のとおり alerter はクリック待ちでブロックします。単純に (...) & でバックグラウンド実行しても、Claude Code の hook ランナーが子プロセスの終了を待ってしまい、Claude 本体が WAIT 状態のまま止まる(トークンも消費し続けてしまう)という問題がありました。
nohup ... & で SIGHUP を無視させ、さらに disown でジョブテーブルから外すことで、hook プロセスから完全に切り離せます。これにより、通知の表示・クリック待ちとは独立して Claude が動作を継続できるようになりました。
9. 通知のカスタマイズ
特定ツールの通知をスキップする
VSCode の「Edit Automatically」などセッションレベルで自動許可しているツールは settings.json に記録されないため、~/.claude/notify_skip_tools.txt に 1 行 1 ツール名で記載する仕組みを入れてあります:
# セッションレベルで自動許可しているツール名を 1 行 1 つで記載 Edit
もしくは notify_pretool.sh の先頭付近にあるスキップリスト(Glob|Grep|TodoWrite|...)に追記する方法でも同等です。
サウンド
macOS 標準のサウンド名を指定できます: Ping, Purr, Glass, default, Basso, Blow, Bottle, Frog, Funk, Hero, Morse, Pop, Sosumi, Submarine, Tink。
--sender(通知アイコン)
--sender に Bundle ID を指定すると通知アイコンが変わります。現在は com.microsoft.VSCode を指定して VSCode アイコンを表示しています。
| アプリ | Bundle ID |
|---|---|
| VSCode | com.microsoft.VSCode |
| Cursor | com.todesktop.230313mzl4w4u92 |
| Terminal | com.apple.Terminal |
| iTerm2 | com.googlecode.iterm2 |
| Ghostty | com.mitchellh.ghostty |
ただし --sender を指定すると、そのアプリの macOS 通知設定に依存することになります。対象アプリの通知を OFF にしていると通知が表示されなくなるため注意が必要です。
まとめ
本記事では、Claude Code の Hooks 機能と alerter を組み合わせて、
- タスク完了・許可待ち・入力待ちのデスクトップ通知を出す
- 通知クリックでプロジェクトの VSCode ウィンドウを自動でアクティブにする
- VSCode 拡張でも
PreToolUsehook で許可待ち通知を取りこぼさない
というセットアップ方法と、その過程で踏んだプロセス管理の落とし穴(ゾンビ化 → --group / --timeout / nohup + disown での回収)をご紹介しました。
Claude Code をバックグラウンドで走らせつつ他の作業を並行して進めるスタイルにおいては、「気づかずに長時間止まっていた」という時間を減らすだけで、体感の生産性が目に見えて向上します。CLI と VSCode 拡張で挙動が異なる部分は PreToolUse hook で吸収できるので、Hooks の仕様を把握したうえで自分の開発スタイルに合わせてカスタマイズしてみてください。

最後に
エブリーでは、ともに働く仲間を募集しています。
テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください!