👉

SwiftBarでMacOSのメニューバーにClaudeCodeのステータスを表示する

に公開

このエントリは、次の画像のようにMacOSのメニューバーにClaudeCodeのステータス表示をする設定手順のメモです。

ClaudeCodeの処理中(thinking):
swiftbar-4

swiftbar-1

PermissionRequestの回答待ち(paused):
swiftbar-2

入力待ち(waiting):
swiftbar-3

利用するツールはClaudeCode, SwiftBar, iTerm2です。

https://claude.com/product/claude-code

https://github.com/swiftbar/SwiftBar

https://iterm2.com/

仕組み

全体の仕組みは、以下のグラフのようになります。

  1. ClaudeCodeのHookで、ステータス(PermissionRequestの回答待ちor処理中or入力待ち)をStatusファイルに出力
  2. SwiftBarのプラグインが、Statusファイルに監視してメニューバーにステータスを表示
  3. メニューバーからメニューが選択されたら、該当するセッションのiTerm2のタブをアクティブ化

実装

実装を示します。

ディレクトリ構成

  • $HOME
    • dotfiles
      • swiftbar
        • bin
          • claude-status-hook
          • iterm-activate-session
        • plugins
          • claude-status.1s.sh
    • .claude
      • settings.json

ファイルの内容

dotfiles/swiftbar/bin/claude-status-hook

#!/bin/bash
set -euo pipefail
#
# claude-status-hook - Write per-session Claude Code status
#
# Usage:
#   echo '{"session_id":"...","cwd":"..."}' | claude-status-hook <thinking|waiting|paused|cleanup>
#
# Reads session_id from stdin JSON (via hooks), writes status to
# /tmp/claude-code-sessions/<session_id>. Used by SwiftBar plugin
# to aggregate status across multiple Claude Code sessions.

STATUS_DIR="/tmp/claude-code-sessions"
STATUS="${1:-}"

INPUT=$(cat)

SESSION_ID=$(echo "$INPUT" | /usr/bin/jq -r '.session_id // empty')
CWD=$(echo "$INPUT" | /usr/bin/jq -r '.cwd // empty')

[ -z "$SESSION_ID" ] && exit 0

mkdir -p "$STATUS_DIR"

if [ "$STATUS" = "cleanup" ]; then
    rm -f "$STATUS_DIR/$SESSION_ID"
else
    BRANCH=$(git -C "$CWD" branch --show-current 2>/dev/null || true)
    /usr/bin/jq -n --arg status "$STATUS" --arg cwd "$CWD" --arg iterm_session "${ITERM_SESSION_ID:-}" --arg branch "$BRANCH" \
        '{status: $status, cwd: $cwd, iterm_session_id: $iterm_session, branch: $branch}' > "$STATUS_DIR/$SESSION_ID"
fi

dotfiles/swiftbar/bin/iterm-activate-session

#!/bin/bash
set -euo pipefail
# iterm-activate-session - Activate an iTerm2 session by its unique ID
#
# Usage: iterm-activate-session <ITERM_SESSION_ID>

SESSION_ID="${1:-}"
[ -z "$SESSION_ID" ] && exit 1

osascript <<EOF
tell application "iTerm"
    set found to false
    repeat with w in windows
        repeat with t in tabs of w
            repeat with s in sessions of t
                if unique id of s is equal to "$SESSION_ID" then
                    set found to true
                    set foundWindow to w
                    set foundTab to t
                    set foundSession to s
                    exit repeat
                end if
            end repeat
            if found then exit repeat
        end repeat
        if found then exit repeat
    end repeat

    if found then
        set index of foundWindow to 1
        tell foundWindow to select foundTab
        activate
    end if
end tell
EOF

dotfiles/swiftbar/plugins/claude-status.1s.sh

#!/bin/bash
# claude-status.1s.sh - Aggregate Claude Code session statuses

STATUS_DIR="/tmp/claude-code-sessions"
NOW=$(date +%s)
STALE_SECONDS=7200  # 2 hours

# Cleanup stale files and count sessions
thinking=0
waiting=0
paused=0
total=0

if [ -d "$STATUS_DIR" ]; then
    for f in "$STATUS_DIR"/*; do
        [ -f "$f" ] || continue
        mtime=$(stat -f %m "$f")
        if (( NOW - mtime > STALE_SECONDS )); then
            rm -f "$f"
            continue
        fi
        total=$((total + 1))
        status=$(/usr/bin/jq -r '.status // empty' "$f" 2>/dev/null)
        if [ "$status" = "thinking" ]; then
            thinking=$((thinking + 1))
        elif [ "$status" = "paused" ]; then
            paused=$((paused + 1))
        elif [ "$status" = "waiting" ]; then
            waiting=$((waiting + 1))
        fi
    done
fi

# Display
if [ "$total" -eq 0 ]; then
    echo "💤"
elif [ "$paused" -gt 0 ]; then
    if [ "$total" -eq 1 ]; then
        echo "⚠️"
    else
        echo "⚠️${paused}/${total}"
    fi
elif [ "$thinking" -gt 0 ]; then
    if [ "$total" -eq 1 ]; then
        echo "🤔"
    else
        echo "🤔 ${thinking}/${total}"
    fi
elif [ "$waiting" -gt 0 ]; then
    if [ "$total" -eq 1 ]; then
        echo "🟢"
    else
        echo "🟢 ${total}"
    fi
else
    echo "❓"
fi

# Dropdown menu
echo "---"
if [ "$total" -eq 0 ]; then
    echo "No active sessions"
else
    for f in "$STATUS_DIR"/*; do
        [ -f "$f" ] || continue
        session_id=$(basename "$f")
        status=$(/usr/bin/jq -r '.status // empty' "$f" 2>/dev/null)
        cwd=$(/usr/bin/jq -r '.cwd // empty' "$f" 2>/dev/null)
        iterm_session=$(/usr/bin/jq -r '.iterm_session_id // empty' "$f" 2>/dev/null)
        branch=$(/usr/bin/jq -r '.branch // empty' "$f" 2>/dev/null)
        case "$status" in
            thinking) icon="🤔" ;;
            paused)   icon="⚠️" ;;
            waiting)  icon="🟢" ;;
            *)        icon="❓" ;;
        esac
        dir_name="${cwd##*/}"
        label="${icon} ${dir_name}"
        [ -n "$branch" ] && label="${label} (${branch})"
        if [ -n "$iterm_session" ]; then
            iterm_session_short=${iterm_session##*:}
            echo "${label} | bash=$HOME/dotfiles/swiftbar/bin/iterm-activate-session param1=${iterm_session_short} terminal=false"
        else
            echo "${label}"
        fi
    done
fi

.claude/settings.json

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook waiting"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook cleanup"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook thinking"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook waiting"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook paused"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook thinking"
          }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook thinking"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "idle_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/dotfiles/swiftbar/bin/claude-status-hook waiting"
          }
        ]
      }
    ]
  }
}

設定手順

  1. ClaudeCode, SwiftBar, iTerm2をインストールします.
  2. 上記の各ファイルを作成します。bin配下のスクリプトには実行権限を付与します.
  3. SwiftBarのpluginディレクトリは、$HOME/dotfiles/swiftbar/bin/pluginsにします.

以上。

Discussion