SLOPSHOPPER

lux-mood

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

newguardprocess
v?MITupdated 2026-10-22kenanbendz/claude-mod-luxafor
A shopper browsing a rack in a slop shop
README

lux-mood

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 yougreen rain (matrix)
thinkingbreathing Claude orange (pulse claude)
editing a file (Edit, Write, MultiEdit, NotebookEdit)Claude orange racing round the LEDs (chase claude)
running any other toolcyan wave (wave cyan)
waiting for a permission answerred and blue (police)
session endsoff

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.

Requirements

  • macOS, Apple silicon or Intel. The bundled binary is universal. No Go, no Homebrew, nothing else.
  • A Luxafor Flag plugged directly into the Mac. A monitor's USB hub may not pass it through at all.
  • Claude Code 2.1.275 or later.

Install

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

Updating

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

How it works

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.

The lux CLI on its own

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.

Building lux from source

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

Known limits

  • One light: with several Claude Code sessions open, the last state change wins, and a session ending switches it off.
  • macOS only. Linux would need a build against hidapi plus a udev rule; Windows is untested.
  • The Flag occasionally rejects a USB write (about 1 in 150); lux retries, so you will not notice.

License

MIT

Source 1 files
hooks/register.ts 86 lines
1import 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