Skip to content

About

Your Logitech keyboard, mouse and a Smart Life bulb, showing what Claude Code is doing — colours and styles driven by Claude Code hooks.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

claudgitech

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.

A smart bulb lit green beside a Logitech G213 keyboard and mouse lit red

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.


What it drives

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.

Platforms

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.


Install

git clone https://github.com/tomsleeter/claudgitech.git
cd claudgitech
./install.sh

That 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_local

and check it:

claudgitech doctor     # what is configured and what is reachable
claudgitech test       # cycle every state on every device

Options: ./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)

Styles

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 look

Or 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 device

The 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 lot

G 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.


Using it

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.

In the terminal too

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.

Several sessions at once

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 order

The 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.


How it fits together

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.


Documentation

  • 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.

Licence

MIT — see LICENSE.

Not affiliated with Logitech, Tuya, Amazon or Anthropic. "Logitech", "G HUB", "Smart Life" and "Claude" belong to their respective owners.

About

Your Logitech keyboard, mouse and a Smart Life bulb, showing what Claude Code is doing — colours and styles driven by Claude Code hooks.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages