Claude CodeのHooksと連携させてミラーボールを光らせる。

Claude CodeのHooksと連携させてミラーボールを光らせる。

2026.04.02

はじめに

皆様こんにちは、あかいけです。

Claude Codeを使っていると、承認待ちで処理が止まっていることに気づかず時間を無駄にした経験はありませんか? 私はあります。

Claude Codeはデフォルトだとファイル編集やコマンド実行のたびに承認を求めてきます。
もちろんsettings.jsonsettings.local.jsonのpermissions設定で許可ルールを追加すれば大半は自動化できますが、完全に自動にはできません。

https://code.claude.com/docs/ja/permissions
https://syu-m-5151.hatenablog.com/entry/2025/06/05/134147

例えばBash(npm *)を許可してもnpm install && npm run buildのような複合コマンドは別パターン扱いになったり、sandboxを設定したとしてもプロジェクトフォルダ外のファイルへのアクセスはブロックされたり、設定したつもりでも想定外のパターンで結局承認待ちが発生する...ということが何度もありました。
(セキュリティ面ですべて承認なしにするのは危険すぎるのでしょうがないですが…)

またhooksでosascriptを使ってmacOSの通知を飛ばすようにもしてみました。
ただ、他の作業に集中していると通知バナーって意外と見逃すんですよね。

スクリーンショット 2026-03-27 23.45.47

もっと物理的に、否応なく気づける通知が欲しい…。
そんな時、部屋の片隅にあるミラーボールが目に入りました。

PXL_20250911_161835060_copy_750x1000

そうだ、ミラーボールを光らせよう。


というわけで今回は、
Claude Codeのhooksを使って、承認待ちが発生したらミラーボールを光らせてみました。

ざっくり概要

仕組みはとてもシンプルです。

  1. Claudeが承認を求めると、hookがSwitchBot APIを叩いてミラーボールの電源をON
  2. 承認後にツールが実行されると、hookがSwitchBot APIを叩いてミラーボールの電源をOFF

つまり、ミラーボールが回っている = Claude Codeが承認を待っている…。
光っていたらPCに戻って承認してあげましょう。

準備するもの

  • ミラーボール
  • SwitchBotスマートプラグ
    • オンオフを制御できれば何でもいいですが、今回はプラグミニJPを使います
  • SwitchBot APIトークン & シークレット
    • SwitchBotアプリの「開発者向けオプション」から取得できます
  • Claude Code

本記事ではClaude Codeの設定やSwitchBot操作用のスクリプトを記載しています。
事前準備として必要なミラーボールとSwitchBotのセットアップについては、以下を参照してください。

https://dev.classmethod.jp/articles/cloud-watch-alarm-mirror-ball-tips/

SwitchBot操作用のシェルスクリプト

Claude Codeのhooksでシェルコマンドを実行するため、Bashスクリプトで実装しました。

認証情報の設定

SwitchBot APIの認証にはトークンとシークレットが必要です。
後述のスクリプトで読む込むため、~/.switchbotrcに環境変数として設定しておきます。

SWITCHBOT_DEVICE_MIRRORBALLは任意の名前でOKで、値は後述のスクリプトで取得するdeviceIdを入れます。

~/.switchbotrc
SWITCHBOT_TOKEN="your_token"
SWITCHBOT_SECRET="your_secret"
SWITCHBOT_DEVICE_MIRRORBALL="XXXXXXXXXXXX"

スクリプト本体

どこでもいいので、以下をPATHが通っているディレクトリに配置しておきます。

~/.local/bin/switchbot.sh
#!/usr/bin/env bash
#
# SwitchBot API v1.1 control script for Plug Mini (JP)
# Usage:
#   switchbot.sh list                - List all devices
#   switchbot.sh on <deviceId>       - Turn on a device
#   switchbot.sh off <deviceId>      - Turn off a device
#   switchbot.sh toggle <deviceId>   - Toggle device state
#
# Configuration:
#   Set SWITCHBOT_TOKEN, SWITCHBOT_SECRET, and device IDs
#   as environment variables, or create ~/.switchbotrc with:
#     SWITCHBOT_TOKEN=your_token
#     SWITCHBOT_SECRET=your_secret
#     SWITCHBOT_MIRRORBALL=XXXXXXXXXXXX
#
#   Device ID can be passed as a raw ID or as a variable name:
#     switchbot on XXXXXXXXXXXX          # raw device ID
#     switchbot on SWITCHBOT_MIRRORBALL  # resolved from ~/.switchbotrc

set -euo pipefail

BASE_URL="https://api.switch-bot.com/v1.1"

# ~/.switchbotrcがあれば読み込む
if [[ -f "${HOME}/.switchbotrc" ]]; then
  # shellcheck source=/dev/null
  source "${HOME}/.switchbotrc"
fi

if [[ -z "${SWITCHBOT_TOKEN:-}" || -z "${SWITCHBOT_SECRET:-}" ]]; then
  echo "Error: SWITCHBOT_TOKEN and SWITCHBOT_SECRET must be set." >&2
  echo "Set them as environment variables or in ~/.switchbotrc" >&2
  exit 1
fi

# デバイスIDを解決する(環境変数名なら間接参照で展開)
resolve_device_id() {
  local input="$1"
  # SWITCHBOT_で始まる場合は環境変数名として間接参照
  if [[ "${input}" == SWITCHBOT_* ]]; then
    local resolved="${!input:-}"
    if [[ -z "${resolved}" ]]; then
      echo "Error: variable ${input} is not defined in ~/.switchbotrc" >&2
      exit 1
    fi
    echo "${resolved}"
  else
    echo "${input}"
  fi
}

# HMAC-SHA256署名を生成する
generate_auth_headers() {
  local token="${SWITCHBOT_TOKEN}"
  local secret="${SWITCHBOT_SECRET}"
  local t
  local nonce

  # 13桁のミリ秒タイムスタンプ
  t=$(python3 -c 'import time; print(int(time.time() * 1000))')
  nonce=$(uuidgen)

  local string_to_sign="${token}${t}${nonce}"
  local sign
  sign=$(printf '%s' "${string_to_sign}" \
    | openssl dgst -sha256 -hmac "${secret}" -binary \
    | base64)

  # グローバル変数として設定
  AUTH_HEADER="Authorization: ${token}"
  SIGN_HEADER="sign: ${sign}"
  T_HEADER="t: ${t}"
  NONCE_HEADER="nonce: ${nonce}"
}

# API GETリクエスト
api_get() {
  local endpoint="$1"
  generate_auth_headers

  curl -s -X GET "${BASE_URL}${endpoint}" \
    -H "Content-Type: application/json; charset=utf8" \
    -H "${AUTH_HEADER}" \
    -H "${SIGN_HEADER}" \
    -H "${T_HEADER}" \
    -H "${NONCE_HEADER}"
}

# API POSTリクエスト
api_post() {
  local endpoint="$1"
  local body="$2"
  generate_auth_headers

  curl -s -X POST "${BASE_URL}${endpoint}" \
    -H "Content-Type: application/json; charset=utf8" \
    -H "${AUTH_HEADER}" \
    -H "${SIGN_HEADER}" \
    -H "${T_HEADER}" \
    -H "${NONCE_HEADER}" \
    -d "${body}"
}

# デバイス一覧を表示
list_devices() {
  local response
  response=$(api_get "/devices")

  if command -v jq &>/dev/null; then
    echo "${response}" | jq '.body.deviceList[] | {deviceId, deviceName, deviceType}'
    echo "${response}" | jq '.body.infraredRemoteList[] | {deviceId, deviceName, remoteType}' 2>/dev/null || echo "(none)"
  else
    echo "${response}"
  fi
}

# デバイスにコマンドを送信(許可されたコマンドのみ)
send_command() {
  local device_id="$1"
  local cmd="$2"

  local body="{\"command\":\"${cmd}\",\"parameter\":\"default\",\"commandType\":\"command\"}"

  local response
  response=$(api_post "/devices/${device_id}/commands" "${body}")

  if command -v jq &>/dev/null; then
    echo "${response}" | jq .
  else
    echo "${response}"
  fi
}

# メイン処理
usage() {
  echo "Usage: $(basename "$0") <subcommand> [args]"
  echo ""
  echo "Subcommands:"
  echo "  list                  List all devices"
  echo "  on <deviceId|varName>     Turn on a device"
  echo "  off <deviceId|varName>    Turn off a device"
  echo "  toggle <deviceId|varName> Toggle device state"
  echo ""
  echo "deviceId: raw ID (e.g. XXXXXXXXXXXX) or variable name from ~/.switchbotrc (e.g. SWITCHBOT_MIRRORBALL)"
  exit 1
}

if [[ $# -lt 1 ]]; then
  usage
fi

case "$1" in
  list)
    list_devices
    ;;
  on)
    [[ $# -lt 2 ]] && { echo "Error: deviceId required" >&2; exit 1; }
    send_command "$(resolve_device_id "$2")" "turnOn"
    ;;
  off)
    [[ $# -lt 2 ]] && { echo "Error: deviceId required" >&2; exit 1; }
    send_command "$(resolve_device_id "$2")" "turnOff"
    ;;
  toggle)
    [[ $# -lt 2 ]] && { echo "Error: deviceId required" >&2; exit 1; }
    send_command "$(resolve_device_id "$2")" "toggle"
    ;;
  *)
    usage
    ;;
esac

なおSwitchBot API v1.1では、HMAC-SHA256署名による認証が必要となります。
そのためトークン・タイムスタンプ・nonceを結合した文字列をシークレットで署名し、リクエストヘッダーに付与しています。

https://github.com/OpenWonderLabs/SwitchBotAPI

デバイスIDの確認

操作対象のSwitchBotプラグのデバイスIDを確認するには、listサブコマンドを使います。

switchbot.sh list

ここで表示されたdeviceIdを、前述の~/.switchbotrcに追記しておきます。

{
  "deviceId": "XXXXXXXXXXXX",
  "deviceName": "ミラーボール",
  "deviceType": "Plug Mini (JP)"
}

Claude Code Hooksの設定

Claude Code Hooksは、エージェントのライフサイクルイベントに応じて任意のシェルコマンドを実行できる機能です。

https://code.claude.com/docs/ja/hooks

トリガーとして使えるHook eventsは色々ありますが、今回使うイベントは以下の2つです。
承認待ちが発生するとミラーボールが回り出し、承認してツールが実行されると消灯するイメージです。

イベント タイミング ミラーボールの操作
PermissionRequest Claudeが承認を求めたとき ON (点灯)
PostToolUse ツールが実行されたとき OFF (消灯)

settings.jsonの配置

スコープはどこでもいいので、settings.jsonを作成します。(以下の場合はプロジェクトルート)
commandは${スクリプト名} ${動作} ${デバイス名の変数名}を指定します。

.claude/settings.json
{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh on SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh off SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ]
  }
}

matcherは空文字列にすることで、すべてのツールの承認待ち/実行完了でhookが発火します。
特定のツールだけに絞りたい場合は、"matcher": "Bash"のようにツール名を指定することもできます。

ミラーボール、光ります。

設定が完了したら、Claude Codeで何か作業をさせてみましょう。
承認待ちが発生した瞬間にミラーボールが回り出し、承認してツールが実行されると止まるはずです。

mirror-ball-and-claude-code

光りました、綺麗だね…。

その他使えそうなHook eventsパターン

Hook eventsを変えれば、他の用途にも使えます。
組み合わせ次第で無数の使い道がありますが、ミラーボールの利用を前提にいくつか思いついたものを挙げます。

Claude Code 作業中

承認待ち通知ではなく、Claudeが作業中かどうかを可視化するパターンです。
ミラーボールが回っている間は「Claudeが頑張ってくれている」のが物理的にわかります。

イベント 操作 意味
UserPromptSubmit ON ユーザーがプロンプトを送信した
Stop OFF Claudeが応答を完了した
.claude/settings.json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh on SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh off SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ]
  }
}

エラー・ディスコ

ツールが失敗するたびにミラーボールが回り出します。
部屋がディスコ状態になったら何かがおかしい…デバッグタイムの可視化です。

イベント 操作 意味
PostToolUseFailure ON ツール実行失敗
PostToolUse OFF 次のツールが成功したら消灯
.claude/settings.json
{
  "hooks": {
    "PostToolUseFailure": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh on SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh off SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ]
  }
}

コンテキスト圧縮の反省を促す

コンテキストが溢れる瞬間を可視化できます。
ミラーボールが回ったらコンテキストが溢れた合図です、反省してください。

イベント 操作 意味
PreCompact ON コンテキストがいっぱいになった
PostCompact OFF 圧縮完了
.claude/settings.json
{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh on SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ],
    "PostCompact": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "switchbot.sh off SWITCHBOT_DEVICE_MIRRORBALL"
          }
        ]
      }
    ]
  }
}

複数のライトを組み合わせる

SwitchBotプラグを3つ用意して、それぞれ緑・黄・赤のライトを制御すれば、Claudeの様々な状態をリアルタイムで可視化できます。

私の手元には一つのミラーボールしかないので試せてはいないですが、
以下のように複数のイベントに連動してライトを点滅させてればきっと楽しい気持ちになれるでしょう。

イベント 状態
SessionStart ON - - セッション開始、待機中
UserPromptSubmit - ON - 考え中...
Stop - OFF OFF 完了、待機に復帰
PermissionRequest - - ON 許可待ち(要対応)
PostToolUse - - OFF 承認/実行後、赤消灯
PostToolUseFailure - - ON ツール失敗
StopFailure - OFF ON APIエラー
SessionEnd OFF OFF OFF 全消灯

実装上の注意点

  • SwitchBot APIにはレート制限(10,000回/日)があります。
    toggleを短時間に連打するようなhook設定には注意してください
  • hookのコマンドは非同期で実行されるため、ONとOFFが短時間に連続して飛ぶ場合、順序が保証されない可能性があります
  • SwitchBot APIのレスポンスは数十~数百ms程度かかるため、瞬間的な点滅は難しいです

さいごに

以上、Claude Codeの通知でミラーボールを光らせる方法でした。
承認待ちの見逃しは地味にストレスですが、ミラーボールが回り出せば嫌でも気づきます。
ミラーボール通知、最強です。🪩

もちろんClaude Code Hooksは今回のようなお遊びだけでなく、通知やログ記録など実用的な用途にも活用できます。
皆様もぜひ、お手元のミラーボール等のIoTデバイスと組み合わせて遊んでみてください。

この記事が誰かのお役に立てば幸いです。

この記事をシェアする

関連記事