Session Beacon

A small USB desktop display for parallel Claude Code sessions — which are working, which are idle, and which are waiting on a permission prompt.

Languages: C++ (firmware), Python (host)

Hardware: Arduino Nano ESP32 or RP2040-Zero, 1.8″ ST7735 TFT

License: MIT (code), CERN-OHL-P-2.0 (enclosure), CC BY 4.0 (docs, photos)

With several Claude Code sessions open across different VS Code windows, the one that matters is usually the one I can’t see: stopped on a permission prompt, waiting for an answer. Session Beacon is a small USB display that sits next to the keyboard and lists every running session on one 160 × 128 screen, with a coloured dot for each session’s state and a timer showing how long it has been there.

Front view of the beacon: header reads BEACON, 2 active, $1.77; rows for bench-metrology (6m) and session-beacon (32s); footer shows 5h 38% and 7d 52%.

It runs end to end on Windows: firmware on the board, a Python daemon on the PC, and Claude Code hooks that report each session’s events to the daemon.

How it reads

Each row is one session: a coloured dot, the session’s label, and the time it has spent in its current state. Colour carries the state rather than a word — blue for working, green for idle, grey while starting, magenta for an error. Long labels lose their middle rather than their end, so two sessions with a shared prefix stay distinguishable.

A session that needs an answer escalates and then relaxes. The dot pulses red for the first two minutes, holds static red until ten minutes, and then drops to amber. A prompt I’ve clearly chosen to leave stops shouting, but it’s still visible. A gentler cyan warning covers a session that has gone idle while background work is still running. If nothing changes, it becomes a full request after five minutes.

The header carries the number of active sessions and their combined cost. The footer switches every four seconds between the highlighted session’s context window and the account-wide five-hour and seven-day usage limits. The same daemon also feeds Claude Code’s own statusline, which ends in a small marker showing whether the device is reachable.

Claude Code statusline reading: Opus 5, bench-metrology, ctx 10%, $3.90, beacon.

Statusline with the device connected — the trailing beacon.

Claude Code statusline reading: Sonnet 5, session-beacon, $0.00, beacon?

beacon? — the daemon is running but can’t see the board. That separates “the hooks are broken” from “the cable is out”.

Architecture

Claude Code hooks call a thin forwarder that POSTs each event to a long-running daemon over local HTTP. The daemon holds per-session state and sends a JSON snapshot to the board over USB serial every second. The board redraws only the fields that changed, and sends a heartbeat back.

Session Beacon architecture On the Windows PC, Claude Code hook events pass to a small beacon-hook forwarder, which posts them over HTTP to the beacon-host daemon. The daemon replies to statusline requests and sends JSON snapshots over USB serial to the firmware on the beacon device, which drives an ST7735 TFT and sends heartbeats back to the daemon. Windows PC Beacon — USB device Claude Code hooks beacon-hook beacon-host daemon firmware ST7735 TFT HTTP POST USB serial · JSON heartbeat statusline reply

Design notes

  • One owner for the serial port. Hooks are short-lived processes. If each one wrote to the port directly, they would contend for it, and no process would hold state between events. A single daemon owns the port, keeps state across events, and debounces redraws.
  • A cheap hook path. The forwarder hands each event to the daemon in about 15 ms. PreToolUse is deliberately left out, which halves the hook cost on the busiest event. PostToolBatch covers the case where a prompt is answered by rejecting the call.
  • Honest attention states. Only subagents reset a session’s attention state. Other background tasks don’t, because they send no signal when they retire. On Windows, the host walks the process tree to notice when a VS Code window has been closed out from under its sessions.
  • Partial repaints. Repainting only the changed fields takes about 28 ms, while a full-screen redraw takes about 900 ms. That difference matters when the host sends a snapshot every second to advance the timers.
  • Recovering from a stuck link. After the device froze once for more than seven hours, all serial output now goes through a queue instead of blocking on the USB buffer. If the return path dies while snapshots keep arriving, a 30-second timer forces the USB connection to reset rather than leaving a frozen screen. Elapsed-time checks use signed 32-bit arithmetic, so they survive the 49-day millis() rollover.

Build

The enclosure is a three-part 3D print, 40 × 60 mm and 19 mm deep, held together by four M2 × 16 socket-head screws. A separate stand tilts it at 25°. Either microcontroller fits the same shell. The SolidWorks, STEP, and 3MF files are in the repository’s enclosure/ folder, and docs/enclosure.md covers printing and assembly.

Source

Firmware, host daemon, hook examples, install scripts, enclosure CAD, and the architecture, protocol, and wiring docs are all in the repository. The host side currently targets Windows.