Claude Code hooksでMacの通知を出して、通知から適切なtmuxペインに飛ぶ

お疲れ様です。@2357gi です。
筆者環境では、Terminal.app で開いた tmux の複数のセッション・ペインで同時並行に Claude Code を動かしてます。
Terminal.app 以外を開いてたり、Terminal.app を開いても裏画面の tmux ウィンドウ/セッションで動いている Claude Code の待機状態に気づかないことが多々あります。 そこで、受付状態になった時にいい感じにMacの通知を出してくれるやつと、その通知から直接待機状態の Claude Code が開かれている tmux ペインを開くやつを作りました。

Claude Code にお願いすればサクッと作れるとは思いますが、後者の 通知から該当の tmux ペインを開くスクリプト の実装で若干はまりどころがあり試行錯誤したので、勘所のまとめとスクリプトそのものを共有します。
(*ただし、筆者環境ではVS codeでも Claude Code を開く為、それ用の設定も入っています )

自分の試行錯誤分、あなたのtokenを節約できたら幸いです🫰

実際に使用しているScriptとインストール方法

以下のスクリプトはgistに纏めてあります。
claude-notification.sh · GitHub

上記のファイルを ~/.claude/hooks/ に配置し実行権限を付与、以下のようにhooksの設定を行えば完成です。

{
  "hooks": {
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-notification.sh"
          }
        ]
      },
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-notification.sh"
          }
        ]
      },
      {
        "matcher": "tool_permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-notification.sh"
          }
        ]
      },
      {
        "matcher": "user_question_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-notification.sh"
          }
        ]
      }
    ]
  }
}

terminal-notifierが寂しかったので $HOME/.claude/icons/claude-ai-icon.png に画像を配置して使ってます。
該当ファイルが存在しなくても動きますが、よしなにやってください。

実装の全体像

Claude Codeのhooksを使用して、受付状態になった時に通知を出すスクリプトを実行しています。 通知をクリックすると特定のコマンドが実行されるようになっており、 通知クリック時のフォーカス処理は以下の流れで実装されています。
通知は定番の terminal-notifier を使用しています。-execute オプションを使用することで通知が押下された時に任意のコマンドを実行できます。

Claude Codeが待受状態になり、hook が発火する
  ↓
terminal-notifier によって通知が出る
  ↓
通知クリック
  ↓
terminal-notifier の -execute オプションによってコマンドを実行
  ↓
Terminal.appのアクティブ化 & tmuxセッション・ペインへのフォーカス

通知側(claude-notification.sh)での呼び出しは以下のようになります:

terminal-notifier \
    -title "Claude Code" \
    -message "$rich_message" \
    -subtitle "$subtitle" \
    -group "claude-code-$SESSION_NAME-$PANE_ID" \
    -contentImage "$ICON_PATH" \
    -activate "com.apple.Terminal" \
    -execute "$HOME/.claude/hooks/focus-tmux-pane.sh '$SESSION_NAME' '$PANE_ID' '$TMUX_SOCKET'"

実装の勘所

-execute オプションでパスが通らない

terminal-notifier-executeオプションで起動されるスクリプトは、最小限の環境変数しか持たないプロセスとして実行されます。通常のシェルセッションとは異なり、/usr/local/bin/opt/homebrew/binなどのパスが含まれていません。

スクリプトの冒頭で明示的にPATHを設定する必要があります。

#!/bin/bash

# Set PATH to ensure tmux is found
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:$PATH"

tmux のソケットに接続できない

複数のtmuxサーバーが動作している環境や、非標準的なソケットパスを使用している場合、tmuxコマンドはデフォルトソケット(/tmp/tmux-$UID/default)に接続しようとします。しかし、実際のClaude Codeセッションは別のソケットで動作している可能性があります。

そこで、hookが発火した時に$TMUX環境変数からソケットパスを抽出し、-Sオプションで明示的に指定します。

通知スクリプト側:

# Extract tmux socket path from $TMUX variable (format: /path/to/socket,pid,session_id)
TMUX_SOCKET=$(echo "$TMUX" | cut -d',' -f1)

フォーカススクリプト側:

SESSION_NAME="$1"
PANE_ID="$2"
TMUX_SOCKET="$3"

# Use the tmux socket to connect to the server
tmux -S "$TMUX_SOCKET" switch-client -t "$SESSION_NAME" -c "$client_tty"

switch-clientが複数クライアントで正しく動作しない

単一のtmuxクライアントでは動作するものの、同じセッションに複数のクライアントが接続している場合に正しくフォーカスできませんでした。

tmux switch-clientコマンドは、どのクライアントを切り替えるか指定しないと、現在接続されているクライアントに対して動作します。しかし、通知クリック時のスクリプトはtmuxセッションの外部から実行されるため、「現在のクライアント」という概念が存在しません。

全てのクライアントを列挙し、それぞれに対して明示的に操作を実行することで解決しました。パワーです。

# Get all tmux clients and switch each one
clients=$(tmux -S "$TMUX_SOCKET" list-clients -F '#{client_tty}' 2>&1)

if [ -n "$clients" ] && [ "$clients" != "error"* ]; then
    echo "$clients" | while read -r client_tty; do
        # Switch the client to the target session
        tmux -S "$TMUX_SOCKET" switch-client -t "$SESSION_NAME" -c "$client_tty" 2>/dev/null
        # Select the target pane (use pane ID directly)
        tmux -S "$TMUX_SOCKET" select-pane -t "$PANE_ID" 2>/dev/null
    done
fi

私は日毎単一のターミナルを用い、そこで tmux を運用しているためこの仕様を落とし所としました。
複数のターミナルを開き、一つのセッションへ複数のクライアントから接続している場合は問題が発生する可能性があります🙇‍♂️


それでは以上とさせていただきます。
良いClaude Code Lifeを👋