cc-buddy-bridge

skill
Security Audit
Warn
Health Warn
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 7 GitHub stars
Code Pass
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
  • Permissions — No dangerous permissions requested
Purpose
This tool bridges the Claude Code CLI to the claude-desktop-buddy Bluetooth Low Energy (BLE) hardware device. It allows the physical desk pet to react to terminal sessions—displaying statuses, mirroring chat, and acting as a physical two-factor authorization button for permission prompts.

Security Assessment
Overall risk is rated as Low. The light code audit scanned 12 files and found no dangerous patterns, hardcoded secrets, or requests for overly broad permissions. The tool operates locally, reading local JSONL logs for message updates and communicating over a Unix socket and Bluetooth to the physical device. Its core function involves managing shell commands by design (intercepting tool calls and acting as an interactive allow/deny prompt for risky operations). Because it operates entirely locally over short-range hardware, the attack surface is minimal and strictly confined to your machine.

Quality Assessment
The project is well-maintained and active, with its most recent code push happening today. It uses the permissive MIT license and is cross-platform, supporting macOS, Linux, and Windows. The codebase includes automated testing via GitHub Actions. The primary limitation is low community visibility; with only 7 GitHub stars, it has not been widely vetted by a large user base. However, its "daily-driven" status indicates regular, active use by the author.

Verdict
Safe to use, provided you are comfortable running a relatively new, low-visibility personal project that interacts with your local CLI environment and Bluetooth hardware.
SUMMARY

Bridge Claude Code CLI sessions to the claude-desktop-buddy BLE hardware without the desktop app.

README.md

cc-buddy-bridge

test
License: MIT
Python: 3.11+
Platforms
Status: daily-driven
PRs: Welcome

Bridge Claude Code CLI sessions to the
claude-desktop-buddy BLE
hardware — without going through the Claude desktop app.

The buddy firmware officially pairs with Claude for macOS/Windows. This project
lets you drive the same hardware from a plain terminal running the claude CLI,
so your desk pet reacts to CLI sessions: sleeps when idle, gets busy when a
tool call runs, blinks when a permission prompt needs your attention, and lets
you approve or deny right from the stick's buttons.

What you get

  • Physical 2FA for risky tools — set defaultMode: bypassPermissions everywhere except the desk buddy. A/B buttons on the stick decide allow/deny for the few operations you flagged on permissions.ask.
  • Smart matcher — auto-allow trivial Bash (ls/cat/grep/...), always-ask risky (rm/curl/git push/...), defer the rest to the stick. TOML-overridable.
  • Live stick HUD — assistant replies mirror to the stick within ~500 ms via a JSONL tailer (no Stop-hook flush race).
  • Statuslinecc-buddy-bridge hud renders battery / encryption / pending prompts in your prompt bar; composes with claude-hud.
  • One-command install + autostartcc-buddy-bridge install --service picks the right backend per OS: launchd (macOS), systemd user unit (Linux), Task Scheduler (Windows).
  • Custom GIF characterscc-buddy-bridge push-character ./pack/ uploads a folder of frames over BLE with chunked flow control.

How it works

claude CLI ──PreToolUse/Stop/etc hooks──▶ Unix socket ──▶ daemon ──BLE NUS──▶ stick
                                                           ▲
                                                           └── tails ~/.claude/projects/*.jsonl
                                                               for tokens & recent messages
  • Hooks (configured in ~/.claude/settings.json) fire on session lifecycle
    events, tool calls, permission requests, and turn boundaries.
  • Each hook is a small Python script that posts the event payload to a local
    daemon over a Unix socket.
  • The daemon aggregates per-session state (total / running / waiting /
    tokens / entries) and pushes heartbeat snapshots to the stick over BLE
    Nordic UART Service, speaking the same JSON wire format as the desktop app.
  • For permission prompts, the hook blocks until the stick's buttons decide
    the outcome, then returns allow / deny to Claude Code.

See REFERENCE.md in the buddy firmware repo
for the full wire protocol.

Install

git clone https://github.com/SnowWarri0r/cc-buddy-bridge
cd cc-buddy-bridge
python3.12 -m venv .venv
.venv/bin/pip install -e .

# Register hooks into ~/.claude/settings.json (makes a .backup copy first):
.venv/bin/cc-buddy-bridge install

# In another terminal, start the daemon:
.venv/bin/cc-buddy-bridge daemon

Windows users: Replace .venv/bin/ with .venv\Scripts\ in the commands above.

Then start any claude session. The daemon scans for a BLE device advertising
a name starting with Claude, connects, and begins pushing state.

To remove the hooks:

.venv/bin/cc-buddy-bridge uninstall

Auto-start on login

Instead of running cc-buddy-bridge daemon manually, install it as a
system service so it starts at login and restarts on crashes.

macOS (launchd)

Install as a user-level launchd agent:

.venv/bin/cc-buddy-bridge install --service

This writes ~/Library/LaunchAgents/com.github.cc-buddy-bridge.daemon.plist
pointed at the venv Python you just installed from, runs it immediately via
launchctl load, and redirects stdout/stderr to
~/Library/Logs/cc-buddy-bridge.log.

To remove it:

.venv/bin/cc-buddy-bridge uninstall --service

Windows (Task Scheduler)

Install as a Task Scheduler task:

.venv/Scripts/cc-buddy-bridge install --service

This creates a task named cc-buddy-bridge-daemon that runs at logon.
Logs are written to %LOCALAPPDATA%\cc-buddy-bridge\daemon.log.

To remove it:

.venv/Scripts/cc-buddy-bridge uninstall --service

Linux (systemd)

The same --service flag installs a user-level systemd unit on Linux:

.venv/bin/cc-buddy-bridge install --service

This writes ~/.config/systemd/user/cc-buddy-bridge.service pointed at the
venv Python you just installed from, then runs systemctl --user daemon-reload and systemctl --user enable --now cc-buddy-bridge.service
so the daemon starts immediately and on every login. View logs with:

journalctl --user -u cc-buddy-bridge.service -f

To remove it:

.venv/bin/cc-buddy-bridge uninstall --service

A few Linux-specific gotchas:

  • BLE needs BlueZ. Make sure the bluetooth service is running
    (systemctl status bluetooth) and your user is in the bluetooth
    group (sudo usermod -aG bluetooth $USER, then log out and back in).
    Without that, you'll see
    org.freedesktop.DBus.Error.ServiceUnknown ... org.bluez in the
    journal.
  • Survive logout / start at boot. The user manager exits with your
    last session by default, which stops the daemon. Run
    loginctl enable-linger $USER once if you want the unit to start at
    boot and persist after logout.

Tested on Ubuntu 22.04 LTS. Should work on any distro with a systemd user
manager (Fedora 39+, Debian 12+, Arch, etc.) — please open an issue if
your distro needs a tweak.


cc-buddy-bridge status reports both hook and service status.

Show the stick's state in Claude Code's status line

cc-buddy-bridge hud prints a compact one-line summary (battery,
encryption, pending prompts). Plug it into your ~/.claude/settings.json:

{
  "statusLine": {
    "type": "command",
    "command": "/path/to/.venv/bin/cc-buddy-bridge hud"
  }
}

For an ASCII-only terminal: cc-buddy-bridge hud --ascii.

Already using claude-hud or
another statusline plugin? You can compose both — wrap them in a small
shell script and concatenate outputs; statusLine accepts multi-line
responses.

Live in iTerm2 — paw print, battery progress bar, encryption lock, running session count:

cc-buddy-bridge hud — paw, full green battery bar, 100%, lock, 1run

Other states the same line goes through:

🐾 🔋 96% 🔒              # healthy, encrypted link
🐾 🔋 12% 🔒 2run         # low battery, sessions running
🐾 ⚠ approve: Bash        # permission prompt waiting on the stick
🐾 ∅                      # stick disconnected (but daemon is alive)
🐾 off                    # daemon not running

Requirements

  • macOS 12+ / Windows 10+ / Linux with BlueZ
  • Python 3.11+
  • A flashed claude-desktop-buddy device (M5StickC Plus)
  • Claude Code CLI

Signal mapping

Buddy field Source
total SessionStart / SessionEnd hooks
running UserPromptSubmit / deferred Stop hooks
waiting PreToolUse hook (while decision pending)
prompt PreToolUse hook payload
msg Derived summary of current state
entries Live JSONL tailer (user prompts / tool calls / assistant text)
tokens/today Sum of usage.output_tokens in JSONL

Firmware quirks we hit (and how we work around them)

The reference firmware has several sharp edges the wire protocol doesn't
warn you about. Documenting them here so you don't re-debug them, and so
the workarounds baked into this codebase have a visible rationale.

1. Non-ASCII bytes crash the BLE stack

The 5×7 Adafruit GFX bitmap font table is ASCII-only; any byte in
0x800xFF (i.e. every UTF-8 continuation byte and emoji leading
byte) indexes past the glyph table and, in enough code paths, hard-
resets the radio task within ~1 s of the heartbeat write.

Workaround: sanitize_for_stick() in protocol.py rewrites
everything outside 0x200x7E (and tab) to ? before sending. CJK
users will see rows of ? on the stick, which is lossy but stable.

2. entries wire order is oldest-first, not newest-first

Firmware's drawHUD treats lines[nLines-1] as the newest (and only
that one gets the highlight colour + bottom-of-window position).
Sending newest-first makes the latest entry land at the top of the
wrapped buffer and clip out of the visible 3-row window.

Workaround: the daemon keeps state.entries newest-first
internally (cheap prepend) but reversed()-iterates when serializing
the heartbeat.

3. evt:"turn" events are silently discarded

REFERENCE.md defines a turn event format, but the firmware's
_applyJson only parses heartbeat fields (time, total, running,
waiting, tokens, tokens_today, msg, entries, prompt). Any
evt payload is parsed and dropped — no error, no display.

Workaround: we mirror the assistant's first text block into the
heartbeat's entries list as a synthetic @ <text> row. The firmware
already renders entries, so no protocol extension is needed.

4. Stop hook fires before the assistant record is flushed to disk

Reading the transcript JSONL from the Stop hook returns the PREVIOUS
turn's content — Claude Code's write to disk is async. Naively this
causes every @-entry to be one turn behind.

Workaround: we ignore Stop for content extraction entirely. The
JSONL tailer already watches transcript files via watchfiles; it
fires an on_assistant_text callback the moment a new assistant
record lands (typically <500 ms). The callback adds the entry
immediately, so the stick shows the reply before the user even
scrolls up in the terminal.

5. Clock mode hides the transcript HUD on turn end

The firmware enters clock-face mode the instant
running==0 && waiting==0 && on_USB_power, bypassing drawHUD
entirely. Our old turn_end handler flipped running to 0 the
moment Claude finished — which made the freshly-emitted @ entry
invisible within the same frame.

Workaround: turn_end schedules an asyncio.Task that sleeps
15 seconds before flipping running to 0. A new turn_begin cancels
the pending task. The stick stays on HUD long enough to read the
reply, then goes to clock on genuine idle.

6. LittleFS is not auto-formatted — push-character fails until factory reset

Fresh firmware calls LittleFS.begin(false) (no format-on-fail), so an
uninitialised partition mounts as 0/0 bytes. The only code path that
calls LittleFS.format() is the on-device factory reset menu
(hold A → settings → reset → factory reset → tap twice).

cc-buddy-bridge push-character detects this via the status ack and
logs an ERROR with the remediation hint. Factory reset is destructive
(wipes settings, stats, bonds) but needed once per stick.

7. blueutil --unpair is unreliable on modern macOS

For a clean BLE pairing test you need to clear both sides' bonds.
blueutil advertises --unpair as EXPERIMENTAL; on macOS Sonoma+
it returns success without actually removing the cached LTK, and a
subsequent reconnect fails with CBErrorDomain Code=14 "Peer removed pairing information".

Workaround: cc-buddy-bridge unpair clears the stick side over
the encrypted channel, but the user has to manually open
System Settings → Bluetooth → Claude-5C66 → ⓘ → Forget This
Device
on the macOS side. After that, the next reconnect triggers
a fresh 6-digit passkey pairing.

Status

Daily-driver complete — the author runs it on every Claude Code session.

Battle-tested infra

  • Fresh BLE pairing — MITM + bonding + DisplayOnly passkey, end-to-end
  • Reconnection — exponential backoff + multi-daemon guard (refuse to start if another instance owns the socket)
  • Folder push — chunked flow control, 1.8 MB pack cap, per-chunk acks
  • Stick status polling — battery / encryption / fs free every 60 s
  • Logging — rotating file, per-component levels, structured permission round-trip traces

Tests + CI

  • 98 unit tests covering state, protocol, installer, hud, matchers, JSONL tailer, folder push, service backends
  • GitHub Actions matrix across Python 3.11 / 3.12 / 3.13

Backlog

  • Open an issue — any rough edge, a quirk you hit, a feature you want, a platform that misbehaves

License

MIT. See LICENSE.

Reviews (0)

No results found