
Claude CodeのHooksと連携させてミラーボールを光らせる。
はじめに
皆様こんにちは、あかいけです。
Claude Codeを使っていると、承認待ちで処理が止まっていることに気づかず時間を無駄にした経験はありませんか? 私はあります。
Claude Codeはデフォルトだとファイル編集やコマンド実行のたびに承認を求めてきます。
もちろんsettings.jsonやsettings.local.jsonのpermissions設定で許可ルールを追加すれば大半は自動化できますが、完全に自動にはできません。
例えばBash(npm *)を許可してもnpm install && npm run buildのような複合コマンドは別パターン扱いになったり、sandboxを設定したとしてもプロジェクトフォルダ外のファイルへのアクセスはブロックされたり、設定したつもりでも想定外のパターンで結局承認待ちが発生する...ということが何度もありました。
(セキュリティ面ですべて承認なしにするのは危険すぎるのでしょうがないですが…)
またhooksでosascriptを使ってmacOSの通知を飛ばすようにもしてみました。
ただ、他の作業に集中していると通知バナーって意外と見逃すんですよね。

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

そうだ、ミラーボールを光らせよう。
というわけで今回は、
Claude Codeのhooksを使って、承認待ちが発生したらミラーボールを光らせてみました。
ざっくり概要
仕組みはとてもシンプルです。
- Claudeが承認を求めると、hookがSwitchBot APIを叩いてミラーボールの電源をON
- 承認後にツールが実行されると、hookがSwitchBot APIを叩いてミラーボールの電源をOFF
つまり、ミラーボールが回っている = Claude Codeが承認を待っている…。
光っていたらPCに戻って承認してあげましょう。
準備するもの
- ミラーボール
- SwitchBotスマートプラグ
- オンオフを制御できれば何でもいいですが、今回はプラグミニJPを使います
- SwitchBot APIトークン & シークレット
- SwitchBotアプリの「開発者向けオプション」から取得できます
- Claude Code
本記事ではClaude Codeの設定やSwitchBot操作用のスクリプトを記載しています。
事前準備として必要なミラーボールとSwitchBotのセットアップについては、以下を参照してください。
SwitchBot操作用のシェルスクリプト
Claude Codeのhooksでシェルコマンドを実行するため、Bashスクリプトで実装しました。
認証情報の設定
SwitchBot APIの認証にはトークンとシークレットが必要です。
後述のスクリプトで読む込むため、~/.switchbotrcに環境変数として設定しておきます。
SWITCHBOT_DEVICE_MIRRORBALLは任意の名前でOKで、値は後述のスクリプトで取得するdeviceIdを入れます。
SWITCHBOT_TOKEN="your_token"
SWITCHBOT_SECRET="your_secret"
SWITCHBOT_DEVICE_MIRRORBALL="XXXXXXXXXXXX"
スクリプト本体
どこでもいいので、以下をPATHが通っているディレクトリに配置しておきます。
#!/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を結合した文字列をシークレットで署名し、リクエストヘッダーに付与しています。
デバイスIDの確認
操作対象のSwitchBotプラグのデバイスIDを確認するには、listサブコマンドを使います。
switchbot.sh list
ここで表示されたdeviceIdを、前述の~/.switchbotrcに追記しておきます。
{
"deviceId": "XXXXXXXXXXXX",
"deviceName": "ミラーボール",
"deviceType": "Plug Mini (JP)"
}
Claude Code Hooksの設定
Claude Code Hooksは、エージェントのライフサイクルイベントに応じて任意のシェルコマンドを実行できる機能です。
トリガーとして使えるHook eventsは色々ありますが、今回使うイベントは以下の2つです。
承認待ちが発生するとミラーボールが回り出し、承認してツールが実行されると消灯するイメージです。
| イベント | タイミング | ミラーボールの操作 |
|---|---|---|
PermissionRequest |
Claudeが承認を求めたとき | ON (点灯) |
PostToolUse |
ツールが実行されたとき | OFF (消灯) |
settings.jsonの配置
スコープはどこでもいいので、settings.jsonを作成します。(以下の場合はプロジェクトルート)
commandは${スクリプト名} ${動作} ${デバイス名の変数名}を指定します。
{
"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で何か作業をさせてみましょう。
承認待ちが発生した瞬間にミラーボールが回り出し、承認してツールが実行されると止まるはずです。

光りました、綺麗だね…。
その他使えそうなHook eventsパターン
Hook eventsを変えれば、他の用途にも使えます。
組み合わせ次第で無数の使い道がありますが、ミラーボールの利用を前提にいくつか思いついたものを挙げます。
Claude Code 作業中
承認待ち通知ではなく、Claudeが作業中かどうかを可視化するパターンです。
ミラーボールが回っている間は「Claudeが頑張ってくれている」のが物理的にわかります。
| イベント | 操作 | 意味 |
|---|---|---|
UserPromptSubmit |
ON | ユーザーがプロンプトを送信した |
Stop |
OFF | Claudeが応答を完了した |
{
"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 | 次のツールが成功したら消灯 |
{
"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 | 圧縮完了 |
{
"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デバイスと組み合わせて遊んでみてください。
この記事が誰かのお役に立てば幸いです。








