Color coded status lights for Claude Code with support for Logitech keyboards, mouse, or any Smart Life / Smart Hue bulb. It runs off Claude Code hooks, so there is nothing to poll and nothing to watch. You can see what your sessions are doing without tabbing through them.
Green means sessions are working. Yellow means one is blocked on a permission prompt. Red means all the work is done. Purple is the one that earns your attention: a session needs you to read its reply and give feedback, context, or the next step.
The bulb is green while the keyboard and mouse are red, which is not a bug. They answer different questions. The keyboard and mouse follow the one session you are sat in front of, and that one has finished. The bulb covers every session you have open, and somewhere in the pile another one is still going. See Several sessions at once.
| Colour | Means | Fires on |
|---|---|---|
| 🟢 Green | Sessions working | UserPromptSubmit, PostToolUse |
| 🟡 Yellow | Needs permission — a question prompt is open | PermissionRequest · Notification:permission_prompt |
| 🟣 Purple | Needs you to read the reply and give feedback, context, or next steps | Stop, after claudgitech ask · Notification:agent_needs_input |
| 🔴 Red | All work complete | Stop |
| ⚫ Off | No live sessions | SessionEnd |
Each state has a style as well as a colour — a hard colour, a breathe, a pulse, or a wave travelling across the keyboard's zones. See Styles.
| Device | Driver | Notes |
|---|---|---|
| Logitech keyboard | keyboard |
Zone lighting. Built and tested on a G213 (5 zones); any zone-lit board works once you declare its layout. |
| Logitech mouse | mouse |
Same daemon, its own device selector. Built and tested on a G PRO Wireless (2 zones). |
| Smart Life / Tuya bulb | tuya_local |
Direct LAN control on tcp/6668. Sub-second, no cloud. |
| Alexa routine | voicemonkey |
Cloud fallback for when local bulb control is impossible. |
| Nothing at all | dummy |
Logs what it would have done. The safe default. |
| Logitech keyboard & mouse | Smart Life bulb | |
|---|---|---|
| Windows + WSL | ✅ G HUB's LED SDK, via a resident C# daemon | ✅ |
| macOS | ✅ via OpenRGB — see docs/macos.md | ✅ |
| Linux | ✅ via OpenRGB | ✅ |
There is no Logitech LED SDK for macOS or Linux — G HUB ships that DLL on Windows only. So off Windows the same drivers write the same state files to a Python daemon that talks to an OpenRGB server instead. Same protocol, same effects, same constants; only the last hop differs.
macOS also ships none of flock, setsid or timeout. claudgitech uses them
where they exist and falls back to shell equivalents where they do not, so
Homebrew is not a requirement.
git clone https://github.com/tomsleeter/claudgitech.git
cd claudgitech
./install.shThat installs into ~/.claudgitech, wires eight Claude Code hooks into
~/.claude/settings.json (backing it up first), drops a /claudgitech slash
command in, and — on WSL, if the .NET 8 SDK and G HUB are both present — builds
and starts the Windows lighting daemon.
Then pick your devices in ~/.claudgitech/config.env:
CL_DRIVER=keyboard,mouse,tuya_localand check it:
claudgitech doctor # what is configured and what is reachable
claudgitech test # cycle every state on every deviceOptions: ./install.sh --no-windows (bulb only), --no-hooks (files only),
--home DIR. Re-running upgrades in place. ./uninstall.sh reverses it;
--purge also deletes your config.
Requirements — bash and python3, plus:
| For | You need |
|---|---|
| the bulb | pip install tinytuya |
| Logitech, on Windows/WSL | G HUB + the .NET 8 SDK |
| Logitech, on macOS/Linux | OpenRGB with its SDK server on, and pip install openrgb-python (the installer does this) |
A colour tells you what; a style tells you how urgently. Every Logitech device supports all of them.
| Style | Looks like | Good for |
|---|---|---|
solid |
hard colour, no animation | a resting state you do not want to notice |
breathe |
slow fade in and out (0.4 Hz) | "your move", calm |
pulse |
fast fade (1.15 Hz) | "I am blocked on you" |
blink |
hard on/off (1.5 Hz) | you genuinely cannot miss it |
wave |
a bright band sliding along the zones | "still running" — reads as alive |
scanner |
a tight dot sweeping over a dim floor | the classic Knight Rider |
comet |
a bright head wrapping round, dragging a tail | busier than wave |
rainbow |
full spectrum rotating across the zones | fun, or "everything is fine" |
off |
dark | a device you want quiet in one state only |
Aliases: hard/static → solid, flash → blink, sweep → wave,
cycle → rainbow.
See it, and set it:
claudgitech styles # the whole current mapping
claudgitech style working wave device=keyboard # persist into config.env
claudgitech style approval blink # all devices
claudgitech show blue style=comet secs=15 # just have a lookOr by hand in config.env — most specific wins:
CL_STYLE_keyboard_working=wave # this device, this state
CL_STYLE_working=solid # this state, everywhere
CL_SPEED_mouse=0.6 # animation rate multiplier, per device
CL_BRIGHTNESS_mouse=60 # ceiling brightness, per device
CL_COLOR_working=#00c8ff # and the colour, shared by every deviceThe defaults are approval=pulse, input=breathe, done=solid, and
working=wave on the keyboard, solid everywhere else.
Colour, speed and brightness resolve the same way, and the logic is configurable too — which event means what, and whether claudgitech should own the device at all for a given state:
claudgitech color working 0066ff device=keyboard # bare hex: an unquoted # is a shell comment
claudgitech speed 0.6 device=mouse
claudgitech release working,input device=keyboard # your own G HUB profile shows instead
claudgitech config CL_EVENT_working ignore # only light up when I'm needed
claudgitech config # see the lotG HUB's own integration panel says "this integration has no options to configure" — that is correct and permanent: the legacy LED SDK has no settings surface to offer. Everything above is where the options live. See docs/logitech.md.
One-zone devices still animate. A travelling style has nowhere to travel on
a single-LED mouse, so it degrades rather than doing nothing: wave → breathe,
scanner/comet → pulse.
The bulb is not a keyboard. It takes a network round trip per frame, so
tuya_local does solid and rainbow at any time, and breathe/pulse/
blink only from an explicit show (which is time-bounded). Anything else
rests on solid. Details in docs/smartlife.md.
claudgitech status live sessions and what each device is showing
claudgitech config every setting, and whether it is yours or a default
claudgitech config K V change one and repaint
claudgitech color <state> <colour> [device=D]
claudgitech speed <n> · claudgitech brightness <0-100>
claudgitech release <state,...|none> [device=D]
hand the device back to your own G HUB profile for
these states instead of painting them
claudgitech styles the state -> style mapping
claudgitech show <what> a colour, a state, a style, rainbow, or clear
claudgitech style <state> <style> [device=D]
claudgitech claim give this session the keyboard and mouse
claudgitech release hand them to the next active session
claudgitech done declare work finished -> red
claudgitech set <state> force approval|input|working|done|off
claudgitech off everything off, forget all sessions
claudgitech test cycle every state
claudgitech probe [mouse] find a device's zone layout
claudgitech devices what the lighting backend can see (OpenRGB)
claudgitech doctor config + reachability check
Inside Claude Code, /claudgitech green, /claudgitech rainbow,
/claudgitech blue wave and /claudgitech clear all work.
claude.ai in a browser can drive the same lights too — it has no hooks, so a
userscript infers state from the network and reports it to a localhost bridge.
claudgitech serve, plus docs/claude-chat.md.
Red is the interesting one. Purple is the honest end of most turns: Claude
stopped, and it is your move. Red means finished — and nothing infers that, so
Claude has to say it by running claudgitech done as its last action. Put that
rule in your CLAUDE.md:
When a task is genuinely finished — all work landed, nothing pending, nothing
waiting on a decision — run `claudgitech done` as the last action before the
closing message. Do NOT run it when handing back a question or stopping
mid-task; purple is correct for those, and it is set automatically.The lights tell you about the room. With several sessions open you also want to know which window is which, so the same state drives a Claude Code status line:
{
"statusLine": {
"type": "command",
"command": "~/.claudgitech/bin/claudgitech-statusline",
"padding": 1,
"refreshInterval": 3
}
}It reads the same per-session state files the lights do, so the terminal and the
hardware cannot disagree, and it honours your CL_COLOR_* overrides.
● working │ my-session │ sleeter-learning │ Opus
▸ Wire claudgitech into the Claude Code CLI UI
The second row is what that session is for. Claude keeps it current:
claudgitech note "one line summary
Any further lines are the detail."The first line is the summary; the rest is the detail, and the row is hyperlinked to it, so a terminal that supports OSC 8 expands it on click. Until a note is set it falls back to the last prompt you typed, so the row is never blank.
State is recorded per session, and each device resolves its own winner:
aggregate— the whole swarm. Green while any agent is still working, so the light only turns amber or red once everything has genuinely stopped. The right setting for a bulb across the room: it answers "can I walk away?"primary— the one session holding the desk lease: the prompt you are actually looking at. The right setting for the keyboard and mouse under your hands.
CL_STRATEGY_keyboard=primary
CL_STRATEGY_mouse=primary
CL_STRATEGY_tuya_local=aggregate
CL_LIGHT_PRIORITY="working approval input done" # aggregate's tie-break orderThe desk lease is claimed by whichever session dispatches first and held
until that session exits or goes 15 minutes without a hook. Recomputing "who is
oldest" on every hook is what made an earlier version flip-flop between
terminals. claudgitech claim takes it by force; release gives it up.
working entries expire after 10 minutes (CL_WORKING_STALE_SECS) because a
subagent that finished never refreshes its entry, and stale green would
otherwise stop the light ever reaching red.
Claude Code hook ──▶ bin/claudgitech resolve state per device,
(8 events) look up style + colour
│
┌─────────────┼─────────────┐
▼ ▼ ▼
drivers/keyboard drivers/mouse drivers/tuya_local.py
│ │ │
└──── one line each ────┐ └──▶ bulb, tcp/6668 on your LAN
▼
sessions/*.state
│
┌─────────────────┴─────────────────┐
▼ ▼
claudgitech.exe (C#) claudgitech-lightd (Python)
Windows: the LED SDK macOS + Linux: OpenRGB
Three deliberate choices are worth knowing about:
The daemon has to be resident. Logitech's LED SDK hands lighting back to G HUB the moment the process that called it exits, so a "set the colour and quit" binary cannot hold anything; OpenRGB is the same story. A small daemon keeps the connection open and does the animating — and refuses to start a second copy of itself, because two daemons painting one keyboard at 16fps does not look like two daemons, it looks like the hardware glitching.
WSL talks to it through files, not a socket. PostToolUse fires on every
tool call, so the hook path has to be nearly free. A ~2ms filesystem write
across the WSL boundary is; spawning a Windows process is not. No socket, no
port, no firewall rule.
Every animation is phased off the wall clock. The bulb and the daemon are
separate programs on opposite sides of that boundary. Both derive hue and
brightness from the same UTC instant with the same constants, so a rainbow
crosses the keyboard, the mouse and the lamp in step, with no messaging and no
clock sync. (RAINBOW_DEG_PER_SEC exists in both windows/src/Effects.cs and
drivers/tuya_local.py — change them together.)
Everything is fire-and-forget: drivers are detached, each has its own lock, and each has a timeout. An unreachable bulb never delays the keyboard, and nothing ever makes Claude Code wait.
- docs/logitech.md — zones, the two backends, adding a device, writing a new style, rebuilding the daemon, troubleshooting.
- docs/macos.md — running on macOS or Linux: OpenRGB setup, HID permissions, launchd/systemd, and what is and is not verified on real hardware.
- docs/claude-chat.md — driving the same lights from claude.ai in a browser, which has no hooks at all: how state is inferred from the network rather than the markup, and the security model for opening a localhost port a web page can see.
- docs/smartlife.md — getting a Smart Life / Tuya bulb under local control, including the one visit to Tuya's cloud you cannot avoid.
MIT — see LICENSE.
Not affiliated with Logitech, Tuya, Amazon or Anthropic. "Logitech", "G HUB", "Smart Life" and "Claude" belong to their respective owners.
