Lights up a Luxafor Flag with what Claude Code is doing: thinking, editing, running tools, waiting for you, idle. Prebuilt, nothing to install.

A Claude Code mod that turns a Luxafor Flag USB light into a status light for Claude. It ships with a prebuilt lux binary, so there is nothing to install besides the plugin.
| Claude is… | The light |
|---|---|
| idle, waiting for you | green rain (matrix) |
| thinking | breathing Claude orange (pulse claude) |
| editing a file (Edit, Write, MultiEdit, NotebookEdit) | Claude orange racing round the LEDs (chase claude) |
| running any other tool | cyan wave (wave cyan) |
| waiting for a permission answer | red and blue (police) |
| session ends | off |
Every state is shown for at least 1.5 seconds, so a tool call that takes 50ms still registers. With no Flag plugged in the mod does nothing.
At the prompt of a terminal session:
/plugin install lux-mood --marketplace kenanbendz/claude-mod-luxafor
Answer y to add the marketplace, then pick a scope (user is the usual one). From a shell:
claude plugin install lux-mood --marketplace kenanbendz/claude-mod-luxafor
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with kenanbendz/claude-mod-luxafor, then Add plugin.
While this repository is private, installing needs read access to it on GitHub and a git credential for github.com on the machine (an SSH key, or gh auth login followed by gh auth setup-git).
The plugin has no pinned version, so every commit on main is a new version. To receive them automatically, turn on auto-update for this marketplace once: in a terminal session run /plugin, open Marketplaces, select claude-mod-luxafor, and choose Enable auto-update. Claude Code then refreshes it in the background shortly after each session starts, and the new version loads on the next launch.
Without auto-update, pull the latest with:
claude plugin update lux-mood@claude-mod-luxafor
hooks/register.ts hooks the engine's session start, turn start, tool calls, permission notifications, turn complete and session end. Each state change runs bin/lux mood <effect>, which stops the effect that is running and starts the next one detached from Claude Code. A file lock in the temp folder is how it finds the running one, so it only ever stops its own kind. Bursts of changes collapse to the latest state, one switch at a time, so two effects never fight for the light.
To change what a state looks like, edit the MOOD table at the top of hooks/register.ts. Any colour or effect from bin/lux list works.
bin/lux is a small CLI you can use without Claude. Copy it somewhere on your PATH if you like.
lux red solid colour (lux list shows them all; claude is Claude orange)
lux max every LED flat out white
lux max strobe strobe at full white
lux pulse <colour> breathe, never below 30%
lux strobe <colour> 70ms on, 50ms off
lux rainbow endless effects: rainbow police disco chase fire rave wave heartbeat thunder matrix
lux chase <colour> chase and wave take a colour too
lux mood <effect> swap the running effect for this one, in the background
lux mood off stop it and switch the light off
lux off
Effects run until Ctrl-C, which switches the light off.
Only needed if you change lux/main.go. Needs Go 1.22+ and the Xcode command line tools (the HID library uses cgo).
cd lux && go build -o ../bin/lux .
For a universal binary, build once per architecture and join them:
cd lux
GOARCH=arm64 CGO_ENABLED=1 go build -o /tmp/lux-arm64 .
GOARCH=amd64 CGO_ENABLED=1 go build -o /tmp/lux-amd64 .
lipo -create -output ../bin/lux /tmp/lux-arm64 /tmp/lux-amd64
codesign -s - -f ../bin/lux
lux retries, so you will not notice.MIT
hooks/register.ts 86 lines1import type { Register, EngineInterface } from 'claude-code'
2
3// What the light does in each state, as a `lux mood` command line. lux mood
4// stops the previous effect, starts the next one detached, and exits 1 at once
5// when no Flag is plugged in, so without the device nothing happens.
6const MOOD = {
7 idle: ['matrix'], // green rain: waiting for you
8 thinking: ['pulse', 'claude'], // breathing Claude orange
9 editing: ['chase', 'claude'], // Claude orange racing round the LEDs: writing code
10 running: ['wave', 'cyan'], // cyan wave: a command or tool is running
11 waiting: ['police'], // red and blue: Claude needs your answer
12 off: ['off'],
13} as const
14type Mood = keyof typeof MOOD
15
16const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
17
18// A state stays on the light at least this long, so a tool call that finishes
19// in 50ms still reads as cyan instead of a flicker. Off is never delayed.
20const DWELL_MS = 1500
21
22// Bursts of state changes coalesce: only the latest wanted mood is applied,
23// one lux mood at a time, so two never race for the light.
24let wanted: Mood = 'off'
25let applied: Mood | undefined
26let appliedAt = 0
27let driving: Promise<void> = Promise.resolve()
28
29function set($: EngineInterface, mood: Mood): Promise<void> {
30 wanted = mood
31 if (wanted !== applied) {
32 driving = driving.then(async () => {
33 while (wanted !== applied) {
34 if (wanted !== 'off') {
35 const wait = DWELL_MS - (Date.now() - appliedAt)
36 if (wait > 0) await $.clock.sleep(wait)
37 }
38 const next = wanted
39 if (next === applied) break
40 await $.process.run([`${$.plugin.root}/bin/lux`, 'mood', ...MOOD[next]]).catch(() => undefined)
41 applied = next
42 appliedAt = Date.now()
43 }
44 })
45 }
46 return driving
47}
48
49export const register: Register = on => {
50 on('session.start', async ($, e, next) => {
51 const started = await next(e)
52 void set($, 'idle')
53 return started
54 })
55
56 on('turn.start', ($, e, next) => {
57 if (e.agentId === undefined) void set($, 'thinking')
58 return next(e)
59 })
60
61 on('tool.call', async ($, e, next) => {
62 if (e.agentId !== undefined) return next(e)
63 void set($, EDIT_TOOLS.has(e.tool) ? 'editing' : 'running')
64 try {
65 return await next(e)
66 } finally {
67 void set($, 'thinking')
68 }
69 })
70
71 on('classic.Notification', { notification_type: 'permission_prompt' }, ($, e, next) => {
72 void set($, 'waiting')
73 return next(e)
74 })
75
76 on('turn.complete', ($, e, next) => {
77 if (e.agentId === undefined) void set($, 'idle')
78 return next(e)
79 })
80
81 on('session.end', async ($, e, next) => {
82 await set($, 'off')
83 return next(e)
84 })
85}
86