cc-yeelight
v0.3.1
Published
Claude Code hooks that turn a Yeelight Screen Light Bar into a focus-aware status indicator — solid status colors when you're at the terminal, breathing pulse when you're away.
Maintainers
Readme
cc-yeelight
Turn your Yeelight Screen Light Bar into a focus-aware status indicator for Claude Code.
- When Claude needs your input (or finishes a turn) and you're not at the terminal, the bar does a single breathing pulse to catch your eye — by default it rides the bar's current color/temperature (a warm-white desk lamp just dims and brightens).
- When you are at the terminal, the pulse becomes a much gentler dim-and-brighten so it doesn't pull focus.
- When you reply, the breathing stops and the bar returns to its previous brightness.
All over the local network using Yeelight's published LAN protocol. No cloud, no Mi Home runtime, no polling.
Requirements
- A Yeelight WiFi light. Tested on the Screen Light Bar Pro —
yeelink.light.lamp15. Any single-channel Yeelight bulb/bar that supportsstart_cfshould work. - LAN Control enabled on the bar (toggle in the Yeelight app — see Setup).
- Node.js ≥ 18 on the machine running Claude Code.
- Claude Code ≥ the version that supports hooks (
Notification,Stop,UserPromptSubmit,SessionEnd). - macOS for focus detection. On Linux/Windows the tool still works — it just always behaves as "unfocused" (i.e., pulses harder) since
lsappinfo/osascriptaren't available.
Install
From the npm registry (recommended):
npm install -g cc-yeelightOr one-shot via npx (no global install — useful for trying it):
npx -y cc-yeelight discoverOr install straight from GitHub if you want the bleeding edge:
npm install -g github:yaroslavkrutiak/cc-yeelightVerify:
cc-yeelight --versionSetup
1. Enable LAN Control on the bar
- Open the Yeelight app (Mi Home works but the Yeelight app is more reliable for this).
- Pair the bar / open it from your device list.
- Open the bar's settings (three-dot / gear icon).
- Toggle LAN Control to ON.
2. Create a config
cc-yeelight initThis writes ~/.config/cc-yeelight/config.json (or $XDG_CONFIG_HOME/cc-yeelight/config.json) with all defaults except host, which is null.
3. Find your bar's IP
cc-yeelight discoverThe bar must be on the same network as your machine for SSDP multicast to reach it. Output looks like:
[
{
"address": "192.168.1.42:55443",
"id": "0x000000001234abcd",
"model": "lamp15",
"name": "Screen Bar"
}
]4. Set the host in config
Open ~/.config/cc-yeelight/config.json and set "host" to the IP (without the port):
{ "host": "192.168.1.42", "port": 55443, ... }5. Verify it works
cc-yeelight testThe bar will softly pulse for ~4s preserving its current color/temperature, then restore its prior state. The command also prints State before and State after; they should match.
6. Install the hooks
cc-yeelight install-hooksThis merges four hook entries into ~/.claude/settings.json (backing up the existing file first):
| Hook event | Command | Effect |
|---|---|---|
| Notification | cc-yeelight notify | needs your input |
| Stop | cc-yeelight stop-event | turn finished |
| UserPromptSubmit | cc-yeelight working | you replied → cancel pulse |
| SessionEnd | cc-yeelight session-end | safety net: stop any pulse |
Existing hooks in your settings (e.g. status-line, formatter, other plugins) are left untouched.
To remove the hooks later:
cc-yeelight uninstall-hooksHow it behaves
| Event | Focused (terminal is frontmost) | Unfocused (you're elsewhere) | |---|---|---| | You typed a message | pulse cancelled, bar restored | pulse cancelled, bar restored | | Claude wants input | gentle pulse (25–55% range) | breathing (20–90%) | | Claude finished | bar restored (no extra notice) | breathing (20–90%) |
Focus is checked per event — switching apps mid-turn is handled correctly on the next event.
Each pulse defaults to a single breath that preserves the bar's current color temperature — a 3000K warm-white desk lamp just dims and brightens, it does not jump to RGB orange/green. Set preserveColor: false on any event to opt into a colored pulse using the configured color. Tune the count per event with cc-yeelight config set notification.breaths N (or stop.breaths, focused.softPulse.breaths). Use 0 for an infinite pulse that only stops on the next event.
State restoration
Each pulse snapshots the bar's state (color mode, CT/RGB, brightness, power) before changing it, then a detached worker explicitly restores set_ct_abx/set_rgb/set_bright after the breath count completes — so color temperature is preserved exactly across notifications. SessionEnd is also wired up as a safety net to clear any in-flight pulse if Claude exits unexpectedly.
Configuration reference
~/.config/cc-yeelight/config.json:
{
"host": "192.168.1.42",
"port": 55443,
"notification": {
"color": 16753920,
"preserveColor": true,
"minBright": 20,
"maxBright": 90,
"durationMs": 1500,
"breaths": 1
},
"stop": {
"color": 65280,
"preserveColor": true,
"minBright": 20,
"maxBright": 90,
"durationMs": 1000,
"breaths": 1
},
"focused": {
"softPulse": {
"color": 16753920,
"preserveColor": true,
"minBright": 25,
"maxBright": 55,
"durationMs": 2000,
"breaths": 1
}
}
}breaths
The number of breath cycles each pulse plays before the front bar auto-restores to its prior state (CT, brightness, color mode all preserved).
breaths: 1(default) — single breath, ~durationMs × 2long, then back to normal.breaths: 0— infinite, keeps breathing until the next event cancels it (UserPromptSubmitfor Notification pulses, manualcc-yeelight cancelotherwise).breaths: N— N full breath cycles, then restore.
The restore happens via a detached background process, so the hook itself returns within ~1.5s — Claude is not blocked.
preserveColor
Controls whether a breath rides on the bar's current color/temperature or switches to the configured RGB color.
preserveColor: true(default) — the breath uses the bar's currentcolor_modeand value, so a warm-white CT desk lamp just dims and brightens.coloris ignored. (If the bar is in HSV mode, it falls back to the configured RGB.)preserveColor: false— the breath uses the configuredcolor(24-bit RGB) for a deliberately tinted alert.
Per-event, e.g.:
cc-yeelight config set notification.preserveColor false # amber alert pulse
cc-yeelight config set stop.preserveColor true # green-on-warm-white offEditing config from the CLI
You can edit any value without opening the file:
cc-yeelight config list
cc-yeelight config get notification.color
cc-yeelight config set notification.breaths 3
cc-yeelight config set focused.softPulse.color 0xff8800
cc-yeelight config set notification.preserveColor false
cc-yeelight config unset focused.softPulse.preserveColor
cc-yeelight config pathHex (0xRRGGBB or #RRGGBB), numbers, true/false, and null are all parsed automatically.
color is a 24-bit RGB integer. Some helpful values:
| Color | Hex | Decimal |
|---|---|---|
| Red | 0xFF0000 | 16711680 |
| Orange | 0xFFA500 | 16753920 |
| Yellow | 0xFFFF00 | 16776960 |
| Green | 0x00FF00 | 65280 |
| Blue | 0x0000FF | 255 |
| Cyan | 0x00FFFF | 65535 |
| Purple | 0x800080 | 8388736 |
| White | 0xFFFFFF | 16777215 |
bright / minBright / maxBright are 1–100. 0 is not legal in Yeelight color flow; the bar would treat it as "no brightness change".
Environment variables
| Variable | Default | Purpose |
|---|---|---|
| CC_YEELIGHT_CONFIG | $XDG_CONFIG_HOME/cc-yeelight/config.json | Override config file path |
| CC_YEELIGHT_HOST | (from config) | Override bar IP |
| CC_YEELIGHT_PORT | 55443 | Override bar TCP port |
| CC_YEELIGHT_FRONT_STATE | $TMPDIR/cc-yeelight-front.json | Override state-snapshot file |
Command reference
Setup:
init Create config at ~/.config/cc-yeelight/config.json
discover SSDP scan for Yeelight devices on the LAN
install-hooks Wire into ~/.claude/settings.json
uninstall-hooks Remove cc-yeelight entries from ~/.claude/settings.json
Configuration:
config list Print full config
config get <dotted.key> Print a single value
config set <dotted.key> <value> Update a value (hex colors accepted)
config unset <dotted.key> Remove a value
config path Print the config file path
Diagnostics:
state Print the bar's current state
focus Print terminal-focus detection result
test Soft pulse for 4s, then restore
Hook commands (also invokable manually):
notify Notification event (Claude needs your input)
stop-event Stop event (Claude finished a turn)
working UserPromptSubmit event (you replied)
session-end SessionEnd event (safety net: clear pulse)
cancel Manual: stop any pulse, restore the barTroubleshooting
No host configured
Run cc-yeelight init, then cc-yeelight discover to find the IP, then edit ~/.config/cc-yeelight/config.json and paste it into host.
Yeelight timeout
Either the bar isn't reachable from your machine or LAN Control is disabled. Check:
- Your machine and the bar are on the same subnet.
- The bar is powered on.
- LAN Control is ON in the Yeelight app (it gets reset after some firmware updates).
- Your firewall isn't blocking outbound TCP/55443.
Quick reachability test: nc -z -w 2 <bar-ip> 55443 && echo OK || echo unreachable.
cc-yeelight discover returns nothing
SSDP multicast doesn't cross subnets. Make sure you're on the same Wi-Fi/SSID/VLAN as the bar. If you are and it still returns nothing, try a wired connection or another machine on the same network — some routers' "AP isolation" blocks multicast between clients. If you already know the bar's IP, you can skip discovery entirely.
Bar starts breathing but never stops
Run cc-yeelight cancel to clear it manually. If it happens repeatedly, your ~/.claude/settings.json may have old hooks pointing at an absolute path from a previous install — run cc-yeelight uninstall-hooks then cc-yeelight install-hooks to refresh them.
Focus detection always says false
Either you're not on macOS, or $TERM_PROGRAM isn't being set by your terminal. Run cc-yeelight focus to see what's detected — if term_program is <unset>, your terminal isn't exporting it. tmux's default-terminal setting sometimes overwrites it; ensure your .tmux.conf does not clobber $TERM_PROGRAM.
How it talks to the bar
The Yeelight LAN protocol is a newline-delimited JSON-RPC over TCP/55443. Each request looks like:
{"id":1,"method":"set_bright","params":[50,"smooth",500]}cc-yeelight uses these methods:
get_prop— read current state (power, color mode, CT, RGB, brightness)set_power,set_rgb,set_ct_abx,set_hsv,set_bright— restore prior statestart_cf/stop_cf— color flow (the breathing animation)
Discovery uses SSDP on UDP multicast 239.255.255.250:1982 (note: 1982, not the standard 1900) with ST: wifi_bulb.
Reference: Yeelight Inter-Operation Spec.
Compatibility
| Surface | Status |
|---|---|
| Claude Code hooks | Notification, Stop, UserPromptSubmit, SessionEnd — installed via cc-yeelight install-hooks. |
| Yeelight device | Tested on Yeelight Screen Light Bar Pro (yeelink.light.lamp15). Any Yeelight WiFi light that exposes start_cf / set_ct_abx / set_rgb over the LAN protocol should work — color/temperature breath rides whichever color_mode the bar reports. |
| Node.js | ≥ 18 (uses node:net, node:dgram, node:fs, node:child_process — no native modules, no transitive dependencies). |
| macOS | Focus detection (isTerminalFocused) uses lsappinfo / osascript. Tested on macOS 13+. |
| Linux / Windows | The tool runs and pulses the bar correctly, but focus detection is unavailable — it always behaves as "unfocused" (the louder pulse). Configure notification / stop profiles accordingly. |
| Tested terminals | Apple Terminal, iTerm2, WezTerm, Ghostty, Warp, Alacritty, kitty, Tabby, Hyper, VS Code / Cursor integrated terminals. Detection keys off $TERM_PROGRAM. |
Known limitations
- Overlapping pulses can briefly desync state. Each pulse schedules a detached restore by
durationMs × breaths × 2 + 250 ms. If a second event fires inside that window (notify → notify, or notify → stop), the older restore may complete after the newer one and leave the bar slightly dim until the next event corrects it. Runcc-yeelight cancelif it ever sticks. A generation-token fix is on the roadmap. - macOS-only focus detection. Linux/Windows users get the unfocused profile for every event. Contributions adding
xdotool/wmctrl/ Windows focus detection are welcome. - State snapshot lives in
$TMPDIR. On macOS this is/var/folders/...and survives reboots but can be purged on low disk. If the bar is reset or its state changes outside cc-yeelight, the next pulse may restore stale values; trigger a freshcc-yeelight cancelto clean up. - Hook timeout budget is 8 s. Focus detection probes have a combined ~2.7 s synchronous worst case (
lsappinfo1.2 s +osascript1.5 s) before TCP work begins. On a slow LAN this can approach the limit. - HSV bars fall back to RGB. Yeelight's
start_cfcolor flow doesn't support HSV mode (3) directly; if your bar reportscolor_mode: 3, the breath uses the configured RGBcolorinstead.
Development & contributing
git clone https://github.com/yaroslavkrutiak/cc-yeelight.git
cd cc-yeelight
node bin/cc-yeelight.js --versionThere are no build steps and no runtime dependencies. To exercise against a real bar, point CC_YEELIGHT_HOST at it and run:
CC_YEELIGHT_HOST=192.168.1.42 node bin/cc-yeelight.js testPull requests are welcome — please keep the package dependency-free and the CLI under one file. For bigger changes (new platforms, new device families) open an issue first so we can sketch the surface together.
Bug reports and feature requests: https://github.com/yaroslavkrutiak/cc-yeelight/issues.
Changelog
See CHANGELOG.md for the version history.
Uninstall
cc-yeelight uninstall-hooks
npm uninstall -g cc-yeelight
rm -rf ~/.config/cc-yeelightLicense
MIT — see LICENSE.
