Turn the macropad's keys into live status lights for your Claude Code sessions: blue while an agent works, amber when it wants a decision, green when it's done. Same idea as the Codex integration, except it runs on Linux, needs no ChatGPT app, and watches whatever sessions you choose.
Built by reverse-engineering the firmware's RPC service. The device is driven directly over raw HID — the vendor app doesn't even need to be running.
Work Louder's 0.6 firmware brought "agent supervision" to the Creator Micro, but it's wired to the ChatGPT desktop app — and it's for its agent, not yours. The vendor app greys the layer out on Linux and tells you the feature isn't available here.
It turns out the LEDs aren't gated on any of that. A key becomes a status slot
purely because its keycode is KV_OAI_AG00…AG05, and v.oai.thstatus drives
those slots over plain JSON-RPC. Any layer can have them, any program can drive
them. Everything else here follows from that.
The ChatGPT app has since shipped a Linux build, and it drives the very same
six slots. That's fine — this daemon only ever writes the slots it owns, and
fills them from the opposite end, so the two run side by side. See
docs/PROTOCOL.md for why per-layer status is impossible.
- Live status for up to 6 sessions (one slot per project directory), driven by Claude Code hooks — no polling of anything, no wrapper around the CLI
- A background daemon that only talks to the device when something changes, survives unplugging, and reconnects on its own
- The ring lit with whatever is most urgent, so a session waiting on you is
visible from across the room without reading four keys (
--no-ringto skip) - A safe config writer that reads every write back and compares it byte for byte, because this firmware truncates oversized writes without saying so
- A protocol write-up (
docs/PROTOCOL.md) covering the RPC methods, the config format, and the traps that cost me a frozen device
It is not limited to Claude Code. Anything that can write a small JSON file can drive the lights.
- A Creator Micro 2, firmware v0.6.1 or later (VID
303A, PID8298) - Linux with
hidraw, Python 3.9+, no extra packages - Read/write access to the device node — see udev
Only tested on the hardware named above, on Ubuntu 26.04 under GNOME/Wayland, over both USB and Bluetooth LE.
git clone https://github.com/coeckmath-maker/creator-micro-agent-status
cd creator-micro-agent-status
./install.shThat copies the scripts to ~/.local/share/wl-agent-status/, installs a
--user systemd unit, and prints the hook configuration to add to
~/.claude/settings.json.
Then put KV_OAI_AG00…AG03 on four keys of a layer (see
Configuring the layer) and you're done.
Bluetooth HID sits on bus 0005, so match on KERNELS rather than the USB
attributes:
# /etc/udev/rules.d/99-worklouder.rules
KERNEL=="hidraw*", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="8298", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="0005:303A:*", MODE="0660", TAG+="uaccess"
sudo udevadm control --reload-rules && sudo udevadm triggerClaude Code hooks ──► record_state.py ──► state.json
│
▼
agent_status_daemon.py
│ JSON-RPC over hidraw
▼
Creator Micro 2 LEDs
Four hooks (SessionStart, UserPromptSubmit, Stop, Notification) write a
one-line state per session. The daemon groups sessions by working directory,
keeps the worst state per project, assigns each project a stable LED slot, and
pushes a frame only when something actually changed.
Hooks reload without restarting your session, so you can wire this up mid-flight.
| State | Colour | Trigger |
|---|---|---|
working |
blue | you sent a prompt |
awaiting-approval |
amber | a permission prompt is up |
unread |
green | the turn finished |
idle |
white | session started, nothing pending |
error |
red | set it yourself |
The band around the case takes the most urgent state on the board, dimmed to
0.35 — a wide diffuse surface at full brightness dominates a desk instead of
informing it. idle deliberately leaves it dark: a light that is always on
tells you nothing, and this one sits on top of whatever ambient lighting you
chose yourself.
That last part is the catch. The ring is a property of the device, not of a
layer — the firmware has nowhere to store a per-layer version — so driving it
overrides your own setting on every layer, and it is still doing so while you
are on some other one. If you would rather keep your lighting, run the daemon
with --no-ring:
systemctl --user edit --full wl-agent-status.service # add --no-ring to ExecStartThe daemon hands the ring back when it stops. It does not hand the keys back: another program may own the slots it never touched.
The device's own flash is the source of truth — the vendor app reads it on connect and overwrites its local cache. Editing the host-side files does nothing on its own.
# read the current config off the device
python3 src/write_config.py --read keymap.json > keymap.json
# edit it, then write it back (verified by read-back)
python3 src/write_config.py keymap.json keymap.jsonPut KV_OAI_AG00…AG03 on the keys you want as status lights — ideally ones
with transparent keycaps.
Keep
keymap.jsonunder about 5.5 KB. Past roughly 6.1 KB the firmware truncates writes silently, and a write past that point froze my device hard enough to need the internal reset button.write_config.pyalways reads back and will tell you if it doesn't match. Details indocs/PROTOCOL.md.
Using KV_OAI_ keycodes makes the layer read-only in the vendor app, and
disables its per-layer lighting. Both are structural, both are documented
in the protocol notes. Configure that layer from scripts instead.
python3 src/listen.py 60 # dump everything the device emits for 60s
python3 src/wl_device.py working awaiting-approval unread idle # force colours
python3 src/wl_device.py --ring awaiting-approval # force the ring
python3 -m unittest discover -s test # no hardware needed
journalctl --user -u wl-agent-status -flisten.py is how the notification format above was found. If you're exploring
this firmware yourself, start there.
Things that cost me hours, in case they cost you fewer:
- The app force-enables your screen reader. It sets
IsEnabledandScreenReaderEnabledonorg.a11y.Statuswhenever it thinks it's on Wayland — which starts Orca, which reads every key you press aloud. Launch it withXDG_SESSION_TYPE=x11and it stops.--ozone-platform=x11alone is not enough; it doesn't change the variable the app checks. CMD_STEPsmart actions are refused until you tick the consent box in the app's settings.TEXT_STEPsmart actions needxdotool, which cannot type into native Wayland windows anyway. Use keyboard macros — they're real HID keystrokes and work everywhere.- Keycodes are physical positions, not letters. On AZERTY,
KC_Qtypesa. See the table in the protocol notes. - After killing the app, delete
~/.config/input/Singleton{Lock,Cookie,Socket}or it silently refuses to start again.
Protocol work cross-checked against two MIT-licensed projects that emulate or drive this hardware:
Two more appeared in September 2026, from macOS:
00cyre/creator-micro-kit (a Node
library for this RPC service) and
00cyre/claude-code-keypad (the
same idea as this project, reading Claude Code's transcripts instead of its
hooks). They go through a Swift IOKit bridge, so neither runs here — but they
found the same central fact independently, and v.oai.rgbcfg is documented
there first. What that method wants on the wire, though, is not what they say:
see docs/PROTOCOL.md.
Not affiliated with Work Louder or OpenAI. Hardware is theirs; the mistakes here are mine.
MIT — see LICENSE.