Make the mirror ball shine by connecting it with Claude Code Hooks.

Make the mirror ball shine by connecting it with Claude Code Hooks.

2026.04.02

This page has been translated by machine translation. View original

Introduction

Hello everyone, I'm Akaike.

When using Claude Code, have you ever wasted time not realizing that processing was stuck waiting for approval? I have.

Claude Code by default asks for approval every time it edits files or executes commands.
Of course, you can automate most of this by adding permission rules in settings.json or settings.local.json, but it's impossible to fully automate everything.

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

For example, even if you permit Bash(npm *), compound commands like npm install && npm run build are treated as different patterns, or even if you set up a sandbox, access to files outside the project folder is blocked. Despite configuring settings, I've experienced many situations where unexpected approval requests still appear.
(From a security perspective, it's unavoidable that allowing everything without approval would be too dangerous...)

I also tried using osascript in hooks to send macOS notifications.
However, when focused on other work, notification banners are surprisingly easy to miss.

Screenshot 2026-03-27 23.45.47

I wanted a physical, unavoidable notification...
That's when I noticed the disco ball sitting in the corner of my room.

PXL_20250911_161835060_copy_750x1000

That's it, let's light up the disco ball.


So in this article,
I used Claude Code hooks to make a disco ball light up when approval is pending.

Quick Overview

The mechanism is very simple.

  1. When Claude requests approval, a hook calls the SwitchBot API to turn ON the disco ball
  2. When the tool executes after approval, a hook calls the SwitchBot API to turn OFF the disco ball

In other words, Disco ball spinning = Claude Code is waiting for approval...
If it's lit up, go back to your PC and give your approval.

What You'll Need

  • A disco ball
  • SwitchBot Smart Plug
    • Anything that can be controlled on/off works, but I'll use the Plug Mini JP for this example
  • SwitchBot API token & secret
    • Available from the "Developer Options" in the SwitchBot app
  • Claude Code

This article describes Claude Code settings and scripts to operate SwitchBot.
For the prerequisite setup of the disco ball and SwitchBot, please refer to the following:

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

SwitchBot Control Shell Script

I implemented a Bash script to execute shell commands with Claude Code hooks.

Setting Up Authentication Information

SwitchBot API authentication requires a token and secret.
Set them as environment variables in ~/.switchbotrc for the script to read.

SWITCHBOT_DEVICE_MIRRORBALL can be any name, and its value should be the deviceId obtained from the script described below.

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

The Script

Place the following script in any directory included in your 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"

# Load ~/.switchbotrc if it exists
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

# Resolve device ID (indirect reference if environment variable name)
resolve_device_id() {
  local input="$1"
  # If starts with SWITCHBOT_, treat as an environment variable name
  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
}

# Generate HMAC-SHA256 signature
generate_auth_headers() {
  local token="${SWITCHBOT_TOKEN}"
  local secret="${SWITCHBOT_SECRET}"
  local t
  local nonce

  # 13-digit millisecond timestamp
  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)

  # Set as global variables
  AUTH_HEADER="Authorization: ${token}"
  SIGN_HEADER="sign: ${sign}"
  T_HEADER="t: ${t}"
  NONCE_HEADER="nonce: ${nonce}"
}

# API GET request
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 request
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}"
}

# Display device list
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 to device (only allowed commands)
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
}

# Main processing
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

Note that SwitchBot API v1.1 requires HMAC-SHA256 signature authentication.
Therefore, we need to sign a string combining the token, timestamp, and nonce with the secret, and include it in the request header.

https://github.com/OpenWonderLabs/SwitchBotAPI

Checking the Device ID

To check the device ID of your SwitchBot plug, use the list subcommand:

switchbot.sh list

Add the deviceId displayed here to your ~/.switchbotrc file:

{
  "deviceId": "XXXXXXXXXXXX",
  "deviceName": "Disco Ball",
  "deviceType": "Plug Mini (JP)"
}

Setting Up Claude Code Hooks

Claude Code Hooks allow you to execute arbitrary shell commands based on agent lifecycle events.

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

There are various hook events available as triggers, but for this purpose, we'll use these two:
When approval is pending, the disco ball starts spinning, and when the tool executes after approval, it turns off.

Event Timing Disco Ball Action
PermissionRequest When Claude requests approval ON (Light up)
PostToolUse When a tool finishes execution OFF (Turn off)

Placing settings.json

Create a settings.json file in any scope (in this case, the project root).
For the command, specify ${script_name} ${action} ${device_variable_name}.

.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"
          }
        ]
      }
    ]
  }
}

Setting matcher to an empty string means the hook will trigger for all tools' approval requests/execution completions.
If you want to limit to specific tools, you can specify the tool name like "matcher": "Bash".

The Disco Ball Lights Up

Once the setup is complete, try having Claude Code perform some task.
The moment approval is requested, the disco ball should start spinning, and it should stop when you approve and the tool executes.

mirror-ball-and-claude-code

It's lit up, beautiful...

Other Useful Hook Event Patterns

By changing hook events, this can be used for other purposes.
There are countless applications depending on the combination, but here are a few ideas assuming disco ball usage:

Claude Code Working

Rather than notification for approval, this pattern visualizes whether Claude is working or not.
While the disco ball is spinning, you can physically see that "Claude is working hard."

Event Action Meaning
UserPromptSubmit ON User submitted a prompt
Stop OFF Claude completed the response
.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"
          }
        ]
      }
    ]
  }
}

Error Disco

The disco ball starts spinning every time a tool fails.
If your room turns into a disco, something's wrong... This visualizes debugging time.

Event Action Meaning
PostToolUseFailure ON Tool execution failed
PostToolUse OFF Turn off when next tool succeeds
.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"
          }
        ]
      }
    ]
  }
}

Encouraging Context Compression Reflection

This visualizes the moment when context overflow happens.
When the disco ball starts spinning, it's a sign that the context has overflowed - time to reflect.

Event Action Meaning
PreCompact ON Context is full
PostCompact OFF Compression completed
.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"
          }
        ]
      }
    ]
  }
}

Combining Multiple Lights

If you prepare three SwitchBot plugs and control green, yellow, and red lights respectively, you can visualize various Claude states in real-time.

I only have one disco ball at hand so I haven't tried this, but making multiple lights flash in response to various events as shown below would surely be fun:

Event Green Yellow Red Status
SessionStart ON - - Session start, standby
UserPromptSubmit - ON - Thinking...
Stop - OFF OFF Completed, back to standby
PermissionRequest - - ON Waiting for approval
PostToolUse - - OFF Red off after approval
PostToolUseFailure - - ON Tool failure
StopFailure - OFF ON API error
SessionEnd OFF OFF OFF All lights off

Implementation Notes

  • The SwitchBot API has rate limits (10,000 requests/day).
    Be careful with hook settings that might rapidly toggle the device
  • Hook commands are executed asynchronously, so if ON and OFF are sent in quick succession, the order may not be guaranteed
  • SwitchBot API responses take a few dozen to several hundred milliseconds, making instant flashes difficult

Conclusion

That's how to make a disco ball light up with Claude Code notifications.
Missing approval requests can be stressful, but if a disco ball starts spinning, you'll definitely notice it.
Disco ball notifications are the best. 🪩

Of course, Claude Code Hooks aren't just for fun like this - they can be used for practical purposes like notifications and logging.
Please try combining your own IoT devices with Claude Code hooks.

I hope this article has been useful to someone.

Share this article

Related articles