nimbus-notify
A Python host broker that watches your AI coding-agent sessions — Claude Code, Codex, and Mistral Vibe — via lightweight hooks, and pushes their live status (running / waiting for you / needs approval / done / errored) to a physical display device over serial (USB-CDC) or Bluetooth LE, using a small documented binary protocol (nsn).
If you've got several agent sessions going in parallel across different terminals and projects, nimbus-notify gives you one glanceable place — an LED ring, an e-ink panel, whatever device you point it at — to see which ones are still working and which ones are waiting on you.
┌────────────┐ hooks ┌───────────────┐ nsn wire protocol ┌──────────┐
│ Claude Code│ ─────────▶ │ │ (serial or BLE) │ status │
│ Codex │ ─────────▶ │ nsnotify- │ ─────────────────────▶│ device │
│ Mistral │ ─────────▶ │ broker │ │(your own)│
│ Vibe │ │ │ │ │
└────────────┘ └───────────────┘ └──────────┘
New here? → QUICKSTART.md gets you from install to a reacting device in ~5 minutes (install ·
nimbus-notify install-hooks· connect · verify). The rest of this README is reference (services, bonding, transports, the protocol).
Status
Beta. The core broker, all three harness adapters, and both transports are
implemented and tested (python3 -m pytest). This is a fresh split out of a
private monorepo into its own package — see CHANGELOG.md.
Compatible devices
nimbus-notify speaks a documented, transport-agnostic wire protocol — see docs/protocol.md. Any device that implements the protocol's serial or BLE side can be driven by this broker; nothing here is tied to a specific piece of hardware. If you build (or already have) a microcontroller project with an LED strip, an e-ink panel, or any other status display, point it at this broker.
Install
pip install nimbus-notify
This installs three commands on your PATH:
nimbus-notify-broker— the daemon that maintains session state and talks to your device.led-report— the small CLI that harness hooks call to report events into the broker (fire-and-forget; never blocks your agent).nimbus-notify— setup helper:install-hookswiresled-reportinto your harness config (idempotent, preserves existing hooks);install-allow-rulespre-approves Claude Code's wake-up tools for unattended loops;doctorchecks your setup.
If
pipitself errors before it reaches this package (e.g. a Python 3.14pyexpat/libexpatdylib mismatch on macOS), that's a broken host pip — useuv:uv tool install nimbus-notify. See QUICKSTART.md for the caveats.
To hack on it, install from source instead:
git clone https://github.com/ristllin/nimbus-notify.git
cd nimbus-notify
pip install -e .
Quickstart
- Install the package (above).
- Connect your device to this computer — over USB or Bluetooth. See Connecting your device below (it's one command).
- Wire up the harness(es) you use — see Harnesses below.
- Start the broker (if it isn't already up from step 2):
nimbus-notify-broker # or: --transport ble / --transport auto
- Start (or resume) an agent session in a wired-up harness. Its status should now be reported to the broker, and forwarded to your device.
If you use Claude Code, the fastest path is the bundled slash commands — see Claude Code plugin below.
Connecting your device
The broker talks to your status device one of two ways — you don't need both, pick whichever fits:
Option A — USB cable (simplest)
Plug the device into your computer with a USB cable and run:
nimbus-notify-broker --transport serial # or just: nimbus-notify-broker
The broker auto-detects the port. That's it — no pairing, no setup. Good for a
device that sits on your desk next to the machine. (To pin a specific port:
--transport serial --port /dev/cu.usbmodem101.)
Option B — Bluetooth, no cable (wireless)
If the device is powered from a wall plug / battery and you don't want a USB cable to your computer, use Bluetooth LE. There is nothing to pair in System Settings — macOS bonds on demand the first time the broker touches the device:
- Power on the device and make sure it's in Notifier mode and in radio range. (Notifier is the status-light mode; if the device is in Orchestrator mode, switch it from the device's knob menu or its web page.)
- Run the broker in the foreground, once:
nimbus-notify-broker --transport ble
- The first time, macOS completes a silent "Just Works" bond (no code to type, no dialog). Leave the broker running for a few seconds — the device's ring will start reflecting your sessions once a frame arrives.
Why foreground the first time? macOS only finishes the bond for a process in your normal login session, so don't background it (
nohup … &) for the first connect. After the bond is stored (on both the Mac and the device's flash) every later run is transparent and can be backgrounded or run as a service. Full detail + un-bonding: Bonding the BLE link.
Which do I have? If you flashed the firmware yourself over USB, the cable is
already there — use Option A. If the device is across the desk on its own power,
use Option B. --transport auto picks serial when a board is plugged in at
startup, else Bluetooth.
First time with a brand-new device? Getting the device onto your Wi-Fi, naming it, and choosing Notifier vs Orchestrator is a separate one-time setup done from the device itself (its setup Wi-Fi + a QR code), not from this broker — see the Nimbus First-time setup guide. For plain Notifier-over-Bluetooth you don't even need Wi-Fi — just Option B above.
Harnesses
nimbus-notify supports three AI coding harnesses today. Each harness reports
events (session start, a tool running, waiting on your approval, done,
errored, session end) by calling led-report <harness> <verb> from a hook.
The fastest way to wire any harness is the installer (idempotent, keeps your existing hooks, safe to re-run):
nimbus-notify install-hooks # all harnesses
nimbus-notify install-hooks --harness claude --dry-run # preview
The manual per-harness steps below are the fallback (and what the installer does).
Claude Code
nimbus-notify install-hooks --harness claude writes this for you. To do it by
hand, merge hooks/claude/settings.json's hooks
block into your ~/.claude/settings.json, preserving any hooks you already
have (append to each event's array rather than replacing it). It wires up
SessionStart, UserPromptSubmit, PreToolUse, Notification, Stop,
StopFailure, and SessionEnd. ⚠ The "needs you" ring uses the Notification
event — not PermissionRequest (a Codex event name that Claude Code never emits).
Wake-up allow-rules (unattended loops)
If you run Claude Code unattended (overnight loops, scheduled wake-ups, a headless fleet), pre-approve the wake-up tools so a session can arm and retire its own wake-ups without stopping for a permission prompt:
nimbus-notify install-allow-rules # merge into ~/.claude/settings.json
nimbus-notify install-allow-rules --dry-run # preview the change first
This appends ScheduleWakeup, CronCreate, CronDelete, and CronList to
permissions.allow (idempotent, backs up first, never touches your other
permissions). Why it matters here: Claude Code gates those tools behind an
approval prompt by default. In an unattended session that prompt is itself the
stuck-ring bug: the session parks in AwaitingApproval (an amber "needs you"
segment) waiting on a human who is not watching, and the wake-up never arms.
Pre-approving them lets the loop both arm a wake-up and self-terminate cleanly,
so no segment gets pinned. Skip this if you only run Claude Code interactively;
doctor treats it as advisory, never a failure.
Codex
Merge hooks/codex/hooks.json into
~/.codex/hooks.json, then enable hooks and the notify bridge in
~/.codex/config.toml:
[features]
hooks = true
notify = ["led-report", "codex-notify"]
Mistral Vibe
Vibe has no native session start/stop hook, so the broker also runs a
background watcher over the Vibe session-log root to detect sessions and
infer human-in-the-loop waits (if a tool call starts but doesn't finish
within a timeout, that's treated as "awaiting approval"). On Vibe v2.25+ it
uses the session lease (<save_dir>/active/<id>.lock) for a precise session
start and end.
Unified-harness sessions resolve their cwd from
<save_dir>/unified/<id>/meta.json and share one segment per working directory
with their hook events. While a session holds its lease the watcher heartbeats it,
so a quiet stretch after a tool call starts is not reaped mid-turn. Once the
session is Idle or Done it ages out on the idle TTL like any other and returns on
its next hook. Vibe has no prompt-submitted hook, so a new turn's thinking before
its first tool call still counts as Idle/Done and can age out if it runs past the
TTL.
The simplest way to wire the hooks is the installer (idempotent; it also upgrades a stale file in place):
nimbus-notify install-hooks --harness vibe
That writes the current Vibe v2.21.0+ hook names (pre_tool / post_tool /
post_agent) into ~/.vibe/hooks.toml. For Vibe older than 2.21 add
--vibe-legacy (it writes before_tool / after_tool / post_agent_turn
plus enable_experimental_hooks = true, which 2.21 removed). Run
nimbus-notify doctor afterwards; it detects the installed Vibe version and
tells you exactly what to run if the hook names don't match. The reference
file is hooks/vibe/hooks.toml.
Claude Code plugin
This repo is also a Claude Code plugin (.claude-plugin/plugin.json). If
you install it as a plugin, two slash commands become available:
/nsnotify-setup— installs the Python package, merges the Claude Code hooks automatically, checks whether the broker is running, and prints the manual steps for Codex/Vibe./nsnotify-status— shows current session states without needing to look at the device.
Transports
Pick a transport with --transport:
nimbus-notify-broker --transport serial # default
nimbus-notify-broker --transport ble
nimbus-notify-broker --transport auto # serial if a device is plugged in at
# startup, else BLE
- Serial auto-detects a likely USB-CDC port (Espressif native-USB VID
0x303A, or common USB-UART bridge chips), or pin one explicitly:nimbus-notify-broker --transport serial --port /dev/cu.usbmodem101. - BLE requires your device to be powered on, flashed with firmware that
advertises the nsn BLE service, and in range. Optionally pin a specific
device:
nimbus-notify-broker --transport ble --ble-address <address>(a CoreBluetooth UUID on macOS, a MAC address on Linux). Without--ble-address, the broker scans for the nsn service UUID.
Transport selection happens once at startup — there's no live failover between serial and BLE mid-session in this version.
Session eviction (--ttl)
A session that ends cleanly (/exit, Ctrl-D) frees its ring segment
instantly via the SessionEnd hook. A session that is hard-killed (closing
the terminal, kill, force-quit) never runs that hook, so the broker reaps it
after an idle timeout instead:
nimbus-notify-broker --ttl 120 # default: drop a silent benign session after 120 s
--ttl SECONDSsets the window for benign states (idle / running / done). Lower it for a snappier ring; raise it to keep quiet-but-alive sessions on the ring longer. Floored at 5 s.- Call-to-action states (awaiting approval / awaiting input / error) always
hold 300 s (matching the device's 5-minute attention hold) regardless of
--ttl, so a job that's blocked on you can't quietly disappear from the ring while it's still pending. A session whose reportedpidwas seen alive and is now gone is evicted on the very next sweep, so a killed job never waits out the full hold.
For unattended loops, two more signals keep a segment from getting pinned (both resolve to existing states, so no firmware change is needed):
wakeup: when a session arms a scheduled wake-up (Claude CodeScheduleWakeup/CronCreate, reported via aPostToolUsehook thatinstall-hookswires for you), the turn has handed off to a timer, not to you. The broker resolves that window to a benign Done that ages out on the short TTL, so a Stop-less wake-up window never leaves a lit arc pinned, and the 60-second idle notification during a wake-up wait is not mistaken for a "needs you" prompt.heartbeat: a liveness ping (led-report claude heartbeat --pid $PPID) that refreshes a session's idle timer without changing what the ring shows. A long supervised turn can send it to stay off the reaper; it never relights a finished or pending segment, and it does not extend a resting Idle session or a Done one with no wake-up armed.
See docs/protocol.md for the full host event vocabulary.
Bonding the BLE link (one time)
Recent Nimbus firmware secures the BLE link (bonded + encrypted, LE Secure Connections), so a device won't accept frames from an un-bonded computer — this stops anyone in radio range from painting your ring. Bonding is automatic (macOS "Just Works", no code to type) — but there are two things to know:
- Nimbus does not appear in System Settings → Bluetooth. That list only shows recognized device types (keyboards, mice, audio). A custom BLE peripheral is invisible there by design — don't look for it. Bonding happens on-demand when the broker first touches the device, not by picking it in a list.
- Do the first bond with the broker in the foreground. macOS only completes
a bond for a process running in your normal login session — a fully detached
process (e.g.
nohup … & disown) is too detached and the bond silently fails. So the first time, just runnimbus-notify-broker --transport blein a normal terminal and leave it up for a few seconds. Once bonded, the bond persists on both the device (flash) and your Mac, and every session after that is transparent — you can then run it backgrounded or as a service (below).
To un-bond: on the device, Connectivity → Forget paired devices (or the
FORGETBONDS console command); the Mac side clears on its own next connect.
The firmware also carries a dormant MITM/passkey mode (it shows a 6-digit code on its e-ink screen). It's off by default because macOS won't surface the passkey-entry dialog for a broker-triggered pairing of a custom peripheral, so that pairing can't complete without a companion app.
Running persistently
For day-to-day use you'll want the broker running in the background whenever you're coding, not started by hand each time — it's a long-lived listener the session hooks fire into, so if it isn't running your device just stops updating.
Recommended: --install-service
One command installs (and later removes) the auto-start service — a macOS launchd LaunchAgent or a Linux systemd user unit — that starts the broker at every login/reboot:
nimbus-notify-broker --install-service # enable auto-start
nimbus-notify-broker --uninstall-service # remove it
⚠ macOS + BLE: run nimbus-notify-broker --transport ble in the foreground
once first to complete the "Just Works" bond (a fully-detached process can't
— see Bonding the BLE link); serial needs no
bond. The service uses
--transport auto (serial if a board is plugged at boot, else BLE).
Quick one-off (no service)
To background it for just this session without installing anything — detach it from the terminal, log to a file, and then close the window:
nohup nimbus-notify-broker --transport auto >/tmp/nimbus-notify-broker.log 2>&1 & disown
Same BLE caveat: complete the one-time foreground bond first (a nohup … & disown process is too detached to finish the initial pairing — see
Bonding the BLE link). Verify with
pgrep -f nimbus-notify-broker or nimbus-notify doctor.
The rest of this section documents what --install-service writes, if you'd
rather manage it by hand.
macOS (launchd)
--install-service writes ~/Library/LaunchAgents/com.nimbus-notify.broker.plist
(equivalent to creating it yourself):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.nimbus-notify.broker</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/env</string>
<string>nimbus-notify-broker</string>
<string>--transport</string>
<string>auto</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/nimbus-notify-broker.log</string>
<key>StandardErrorPath</key><string>/tmp/nimbus-notify-broker.log</string>
</dict>
</plist>
Then:
launchctl load ~/Library/LaunchAgents/com.nimbus-notify.broker.plist
Linux (systemd, user service)
Create ~/.config/systemd/user/nimbus-notify-broker.service:
[Unit]
Description=nimbus-notify broker
[Service]
ExecStart=%h/.local/bin/nimbus-notify-broker --transport auto
Restart=on-failure
[Install]
WantedBy=default.target
Then:
systemctl --user daemon-reload
systemctl --user enable --now nimbus-notify-broker
Adjust ExecStart to wherever pip install -e . put the entry point
(check with which nimbus-notify-broker).
The nsn wire protocol
A short summary — full details in docs/protocol.md.
[SOF 0xAA] [LEN] [payload: LEN bytes] [CRC8-MAXIM]
The payload starts with magic byte 0x4E, then a sequence number, then up
to 16 fixed-size segment records (state, hue, animation, LED span) plus a
global brightness — enough for a device to render a full status frame from
a single self-contained packet, no persistent client-side state required.
The reference encoder/decoder is notify/broker/frame.py.
Repository layout
notify/
broker/ frame.py (wire codec), segments.py, server.py, session.py
cli/ led_report.py — the hook-facing CLI entry point
harness/ base.py + claude.py, codex.py, vibe.py adapters
transport/ serial_tx.py, ble_tx.py
state.py shared State/Anim enums + default styling
tests/ pytest suite for the above
hooks/ drop-in hook configs per harness
commands/ Claude Code plugin slash commands
docs/ protocol.md
Contributing
See CONTRIBUTING.md, including the versioning convention used for releases.
License
MIT — see LICENSE. Copyright (c) 2026 Roy Darnell.
Metadata
Release files for nimbus-notify 1.7.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| nimbus_notify-1.7.2.tar.gz | 188.2 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nimbus_notify-1.7.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 256.6 kB
Release files / nimbus_notify-1.7.2.tar.gz
| Download URL | nimbus_notify-1.7.2.tar.gz |
|---|---|
| Size | 188.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
28f715d49cd0a2f6787818749031d6c50600b1d7c5c967969e3daf3448b6f30c
|
|
BLAKE2b-256 checksum How to use checksums |
f0360dd2019374bcd1c71a84a8f8f542d45a75d67960c5d1c164566bc4e6cd0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / nimbus_notify-1.7.2-py3-none-any.whl
| Download URL | nimbus_notify-1.7.2-py3-none-any.whl |
|---|---|
| Size | 68.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d09e1bad5ba552883af122b417fe29947a5b7d325b0a6ad14adbe7ea5283096
|
|
BLAKE2b-256 checksum How to use checksums |
9a2bd30e90c85ad0a5f18a1c6fe10b9fd2644fa061293a6642cccfef1bff7fc4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logSecurity for nimbus-notify 1.7.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for nimbus_notify-1.7.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.