A physical desk light that shows you what Claude Code is doing — at a glance, from across the room.
Claude Code can run shell commands at key moments in its lifecycle (these are called hooks). This project hangs a tiny script on those hooks so a real light on your desk changes color as the agent works:
- 🔴 Red — the agent is working (you just submitted a prompt)
- 🟡 Yellow — the agent is waiting on you (it needs a permission or input)
- 🟢 Green — the agent is idle or finished (session start, or it just stopped)
No more babysitting the terminal. Glance at the light, get on with your day.
📸 The image above is a placeholder. Drop a photo or GIF of your real build into the
images/folder, name itdemo.jpg(overwriting the placeholder), and it'll show up right here.
| Path | Hardware | Soldering? | Best for |
|---|---|---|---|
| ESP32 + LEDs (the hero build) | ~$10–15 of parts | No (breadboard) | Tinkerers who want the real thing |
| USB busy light (no-solder) | Any Luxafor / Kuando / Embrava light | No | "I just want it working now" |
Both paths use the exact same hooks — only the script differs.
Claude Code fires a hook at certain lifecycle events. A hook just runs a
shell command. We point each event at one small script, light.sh, and hand it
a color:
hook event fires → runs light.sh <color> → flips the light
That's the whole idea. The light is fully decoupled from the agent: the script can talk to an ESP32, a USB light, a smart bulb — anything. Swap the script, keep the hooks.
| Hook event | Meaning | Color |
|---|---|---|
SessionStart |
A session started | 🟢 green |
UserPromptSubmit |
You sent a prompt; agent working | 🔴 red |
Notification |
Agent is waiting on you | 🟡 yellow |
Stop |
Agent finished responding | 🟢 green |
Because hooks are fire-and-forget signals, the scripts always exit 0 and use short timeouts — a powered-off light can never block or break the agent.
| Part | Rough price |
|---|---|
| ESP32 dev board | ~$6 |
| 3 × 5 mm LEDs (red, yellow, green) | ~$1 |
| 3 × 220 Ω resistors | ~$1 |
| Breadboard | ~$3 |
| Jumper wires | ~$2 |
| USB cable | you probably have one |
| Total | ~$10–15 |
Full wiring instructions, including LED polarity and resistor notes, are in
docs/wiring.md.
- Flash the firmware. Open
firmware/traffic_light/traffic_light.inoin the Arduino IDE (or PlatformIO). SetWIFI_SSIDandWIFI_PASSWORDat the top to your network, select your ESP32 board, and upload. - Read the IP. Open the Serial Monitor at 115200 baud and reset the
board. It prints its IP address, e.g.
192.168.1.123. - Set
LIGHT_IP. Openscripts/light.shand put that IP in theLIGHT_IPline near the top. - Install the script. Copy it into your Claude config directory:
cp scripts/light.sh ~/.claude/light.sh chmod +x ~/.claude/light.sh
- Add the hooks. Merge the contents of
settings.example.jsoninto~/.claude/settings.json(under the"hooks"key — keep any hooks you already have). - Wire it up. Follow
docs/wiring.md. - Test it:
Each command should change the light. Now start Claude Code and watch it react on its own.
~/.claude/light.sh red ~/.claude/light.sh yellow ~/.claude/light.sh green ~/.claude/light.sh off
- Install the driver:
pip install busylight-for-humans
- Install the script (note we rename it to
light.sh):cp scripts/light-busylight.sh ~/.claude/light.sh chmod +x ~/.claude/light.sh
- Add the hooks — same as above: merge
settings.example.jsoninto~/.claude/settings.json. - Test it:
~/.claude/light.sh red ~/.claude/light.sh off
Same four hooks, same colors — just a different light underneath.
- The light never changes. Test the script by hand first:
~/.claude/light.sh red. If that works but Claude Code doesn't trigger it, re-check thatsettings.example.jsonwas merged correctly into~/.claude/settings.jsonand that~/.claude/light.shis executable. - The hook hangs / the board seems to stall things. Double-check
LIGHT_IPis correct and thatlight.shuses a curl timeout (it ships with--max-time 2). A powered-off board should fail fast, never hang. - Yellow rarely shows up. That's normal. The
Notificationhook only fires when the agent actually pauses for you (a permission prompt or input request), which doesn't happen on every turn.
The script is the only thing that knows about hardware, so the same pattern works for almost any light:
- Smart bulb (Philips Hue, LIFX, Govee): replace each branch of
light.shwith a call to that bulb's CLI or HTTP API. - LED strip (WS2812 / NeoPixel): point the ESP32 firmware at the strip and set a color per request instead of toggling three pins.
As long as your script accepts red / yellow / green / off and exits 0,
the hooks don't care what's on the other end.
Wi-Fi credentials in the firmware are obvious placeholders — replace them with
your own, but don't commit real ones. The .gitignore already excludes
common secret files (*.local, secrets.*, .env). If you'd rather not keep
credentials in the sketch at all, move them into a separate, gitignored header
and #include it.
Built for the Claude Code community. Thanks for taking a look!
MIT — see LICENSE. © 2026 Shahzad Asghar.
