This post was written with Claude Code (Anthropic's claude-opus-4-6 model).
I roped my firmware co-pilot Claude into writing dual-board Arduino code, debugging WiFi parsers, and arguing about blink rates until the LED did exactly what we wanted.
TL;DR
I built a physical LED indicator that shows what Claude Code is doing in real-time. Fast blink means it’s streaming a response. Slow blink means it’s thinking. Solid green means it’s done. Works over USB serial and WiFi, supports two boards (ESP32-C3 and RP2040), and hooks into Claude Code’s event system with a 56-line shell script.
GitHub: github.com/dinhnguyen/ClauBlink
Why
I run Claude Code sessions that sometimes take a while. At home, I don’t want to sit there watching the terminal. I want to step away, play with my kid, do something else. But checking my phone for notifications isn’t great either, because if I’m playing with my son, I’m playing with my son, not staring at a screen.
I wanted something ambient. A light in the corner of my eye that tells me “Claude is still working” or “Claude is done, come back.” Something I can notice without actively checking.
I remembered blink(1), the USB RGB LED that people used for build status and notification alerts. It’s been around since 2012 but has mostly faded out. Then I talked to a friend who does IoT work. He had a bunch of ESP32 and RP2040 boards lying around, lent me a couple, and said “just try it.” So I did.
The States
Four states, each with a distinct visual:
| State | Blink Rate | NeoPixel Color | Meaning |
|---|---|---|---|
| RESPONSE | Fast (200ms) | Red | Claude is streaming |
| WORKING | Slow (800ms) | Blue | Claude is using tools |
| DONE | Solid | Green | Claude finished |
| IDLE | Off | — | Nothing happening |
The blink rate difference matters. The ESP32-C3 has a single-color on-board LED, so you can’t distinguish states by color. Fast vs slow blink is the only way to tell RESPONSE from WORKING without adding an RGB LED.
On the RP2040 with NeoPixels, you get both color and blink rate. Red flashing fast is unmistakably “Claude is talking.” Blue pulsing slowly is “Claude is chewing on something.”
Hooking Into Claude Code
Claude Code has a hooks system. You can run shell commands on events like PreToolUse, PostToolUse, and Stop. The notify.sh script maps these to LED states:
case "$EVENT" in
PreToolUse)
STATE="RESPONSE:${SLOT}"
;;
PostToolUse)
touch "$FLAG_FILE"
STATE="WORKING:${SLOT}"
;;
Stop)
if [ -f "$FLAG_FILE" ]; then
STATE="DONE:${SLOT}"
rm -f "$FLAG_FILE"
else
STATE="RESPONSE:${SLOT}"
fi
;;
esac
There’s a flag file trick here. When Claude uses tools, PostToolUse sets a flag. When Claude stops, the script checks: did Claude use any tools? If yes, show DONE (green). If no, it was a pure text response, so blink RESPONSE instead. This prevents the LED from going green on simple chat messages.
The script tries USB serial first, falls back to WiFi HTTP. So you can have the board plugged directly into your Mac, or sitting across the room on WiFi.
The WiFi Bug
The first WiFi implementation looked fine. The HTTP server responded 200 OK to every request. But the LED never changed.
The bug was embarrassingly simple. The HTTP handler was passing commands like RESPONSE?slot=0 but the state parser expected RESPONSE:0 with a colon delimiter. The query string format sailed right past every if statement without matching anything.
// Before (broken)
server.on("/RESPONSE", []() { handleCommand("RESPONSE?slot=" + server.arg("slot")); });
// After (works)
server.on("/RESPONSE", []() { handleCommand("RESPONSE:" + server.arg("slot")); });
The server happily returned “OK” for every request. It just never actually changed the LED state. Classic “the API returns 200 but nothing happens” bug.
Two Boards, One Codebase
The firmware runs on both ESP32-C3 and RP2040 from the same main.cpp using preprocessor guards. The NeoPixel library is shared, the color mapping is shared, the state machine is shared. Board-specific stuff (WiFi, pin numbers, LED count) lives behind #ifdef.
The ESP32-C3 drives both its on-board single-color LED (GPIO 8) and an optional NeoPixel on GPIO 2. If you wire up a WS2812B, you get RGB colors. If you don’t, the on-board LED still works with blink-speed differentiation.
The RP2040 supports up to 5 NeoPixels on GPIO 16, one per Claude Code session slot. Multi-session support was a late addition but turned out to be useful when I’m running parallel Claude sessions.
What I’d Do Differently
The blink rates took a few tries. The first version had RESPONSE and WORKING both at 200ms, which made them identical on the single-color ESP32 LED. Slowing WORKING to 800ms made the difference obvious from across the room.
I’d also consider adding a buzzer for the DONE state. Sometimes I walk away while Claude is working, and a visual indicator doesn’t help if I’m not looking at it. A short beep on completion would be practical.
Stack
- Firmware: Arduino/PlatformIO, Adafruit NeoPixel
- Boards: ESP32-C3 Super Mini, Waveshare RP2040-Zero
- Host script: Bash, talks serial or HTTP
- Integration: Claude Code hooks (
settings.json)