Build a Hardware Companion for Claude Code Using Anthropic's BLE API
Your Claude Code agent is stuck on a permission prompt and you're not at your desk. Anthropic just released a BLE maker API so you can build a physical device that handles it — here's the full build walkthrough.
Anthropic released claude-desktop-buddy, an open-source Bluetooth Low Energy (BLE) maker API baked into Claude for macOS and Windows. It lets you pair physical hardware — an ESP32, an M5StickC Plus, whatever you build — directly with Claude desktop, so the device receives session events, displays permission prompts, and lets you approve or deny agent tool calls via physical buttons. No cloud relay. No custom server. Just a Bluetooth connection between Claude and your microcontroller.
TL;DR
Enable Developer Mode in Claude desktop, flash your ESP32 or M5StickC Plus with firmware that speaks Nordic UART Service (NUS), and you get a physical ambient display for Claude Code session state and permission prompts — including on-device approve/deny. The full wire protocol (NUS UUIDs, JSON event schemas, folder push transport) is documented in REFERENCE.md in the official repo. The API is a developer feature, not officially supported, but it's real and it works.
What Is claude-desktop-buddy?
claude-desktop-buddy is Anthropic's official reference repository for a BLE maker bridge inside Claude desktop. When you enable Developer Mode, Claude starts advertising a BLE service that maker hardware can pair with and subscribe to.
Anthropic's own framing: "Providing a lightweight, opt-in API is our way of making it easier to build fun little hardware devices that integrate with Claude." The repo surfaced on the Claude AI subreddit last week and generated immediate interest. As Phemex News reported, the macOS and Windows clients now expose a BLE interface under Developer Mode, enabling hardware to interact with session status and permission requests via the Nordic UART Service.
The reference implementation is an ESP32 desk pet:
- Sleeps when Claude is idle
- Wakes when a session starts
- Displays ASCII animations for agent state (thinking, running a tool)
- Gets visibly impatient when a permission prompt is waiting
- Lets you approve or deny the tool call from the device itself
A second example targets the M5StickC Plus, using its built-in display and side button for approve/deny interactions. The firmware supports ASCII character animations per state, and you can upload custom GIF-based characters.
What problem this solves: Developers running long Claude Code sessions miss permission prompts because they're away from their screen. As one developer who built a native Mac session manager noted, the primary pain across 8 simultaneous Claude sessions is "missing agent prompts because I was on the wrong tab." Hardware eliminates the tab problem — the indicator is on your desk, always visible.
When Does Hardware Actually Make Sense?
Hardware is the right call when:
- You're already building ESP32/Arduino projects and want Claude Code integrated into your physical workspace
- You want a persistent ambient display visible regardless of monitor orientation or screensaver state
- You want physical buttons, not touchscreen taps, for approve/deny interactions
- You enjoy hackable, extensible maker projects
Hardware is the wrong call when:
- You need to handle Claude Code's permission prompts while away from your desk (the ESP32 stays on your desk too)
- Your primary pain is remote session access, not ambient desk display
- You want something working in 15 minutes
Be clear about this distinction before you start. The BLE API solves the "ambient desk indicator" problem well. It does not solve the remote access problem.
Prerequisites
- Claude desktop (macOS or Windows) — Developer Mode enabled (covered below)
- Hardware: ESP32 (any variant) or M5StickC Plus
- Toolchain: Arduino IDE 2.x or PlatformIO with ESP32 board support installed
- BLE library: NimBLE-Arduino is recommended over the stock Arduino BLE library for stability
- JSON library: ArduinoJson (v6 or v7)
- Time: Expect 2–3 hours for a first working prototype
- REFERENCE.md: Read this first — it contains the authoritative wire protocol, NUS UUIDs, JSON schemas, and the folder push transport spec
How Does the BLE Protocol Work?
Claude desktop exposes session events over the Nordic UART Service (NUS), a standard BLE profile widely used in maker and embedded projects. NUS gives you a simple two-characteristic serial-over-BLE transport:
| Characteristic | Direction | Standard UUID |
|---|---|---|
| Service | — | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E |
| RX (device → Claude) | Write | 6E400002-B5A3-F393-E0A9-E50E24DCCA9E |
| TX (Claude → device) | Notify | 6E400003-B5A3-F393-E0A9-E50E24DCCA9E |
Your firmware subscribes to notifications on the TX characteristic to receive events from Claude, and writes to the RX characteristic to send responses (permission approvals or denials).
Events your device receives:
- Session start / stop
- Agent status:
thinking,tool,idle - Permission request — includes tool name, input payload, and a
toolUseIDfor your response - Recent messages from the session
What your device sends back:
- Permission approval or denial, keyed to
toolUseID
Events are JSON objects, newline-terminated, streamed over NUS. The exact schemas live in REFERENCE.md — don't guess, read them.
Step 1: Enable Developer Mode in Claude Desktop
Open Claude desktop. Navigate to Settings → Developer and enable the Bluetooth maker API toggle. The exact label varies slightly by Claude version.
Once active, verify with a BLE scanner app (nRF Connect works on both iOS and Android): scan for devices and confirm you see Claude's NUS service UUID (6E400001-B5A3-F393-E0A9-E50E24DCCA9E) in the advertisement list. If it doesn't appear, the toggle isn't enabled or Claude doesn't have Bluetooth permission at the OS level (on macOS: System Settings → Privacy & Security → Bluetooth).
Step 2: Flash the ESP32 Firmware
Below is a minimal sketch illustrating the connection flow and event handling. This is intentionally high-level — fill in the exact JSON schemas from REFERENCE.md before building production firmware.
#include <NimBLEDevice.h>
#include <ArduinoJson.h>
#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define NUS_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define NUS_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
NimBLEClient* pClient = nullptr;
NimBLERemoteCharacteristic* pRxChar = nullptr;
String currentToolUseID = "";
String incomingBuffer = "";
void notifyCallback(NimBLERemoteCharacteristic* pChar,
uint8_t* pData, size_t length, bool isNotify) {
incomingBuffer += String((char*)pData, length);
// NUS frames may arrive in multiple notifications — buffer until newline
int newlineIdx = incomingBuffer.indexOf('\n');
if (newlineIdx == -1) return;
String frame = incomingBuffer.substring(0, newlineIdx);
incomingBuffer = incomingBuffer.substring(newlineIdx + 1);
StaticJsonDocument<512> doc;
if (deserializeJson(doc, frame) != DeserializationError::Ok) return;
const char* type = doc["type"];
if (strcmp(type, "session_start") == 0) {
wakeUp();
} else if (strcmp(type, "session_stop") == 0) {
goToSleep();
} else if (strcmp(type, "permission_request") == 0) {
currentToolUseID = doc["toolUseID"].as<String>();
showPermissionPrompt(doc["toolName"].as<const char*>());
} else if (strcmp(type, "status") == 0) {
showStatus(doc["status"].as<const char*>());
}
}
void sendPermissionResponse(bool approved) {
if (currentToolUseID.isEmpty() || !pRxChar) return;
StaticJsonDocument<128> resp;
resp["toolUseID"] = currentToolUseID;
resp["approved"] = approved;
String payload;
serializeJson(resp, payload);
payload += "\n";
pRxChar->writeValue(payload.c_str(), payload.length());
currentToolUseID = "";
}
void setup() {
Serial.begin(115200);
NimBLEDevice::init("claude-buddy");
// Scan for Claude's service UUID, connect, subscribe to TX notifications
// See REFERENCE.md for the full scan-and-connect flow
NimBLEScan* pScan = NimBLEDevice::getScan();
// ... scan, connect, get service, get TX char, subscribe
}
void loop() {
if (approveButtonPressed() && !currentToolUseID.isEmpty()) {
sendPermissionResponse(true);
}
if (denyButtonPressed() && !currentToolUseID.isEmpty()) {
sendPermissionResponse(false);
}
delay(50);
}
The M5StickC Plus example in the official repo is cleaner for hardware UX — the built-in display handles the status text and the side button maps naturally to approve/deny without external wiring.
Step 3: Map Session Events to Device State
The reference desk pet implements a simple state machine driven by Claude session events. Here's a practical mapping for your firmware:
| Claude Event | Device State | Suggested Behavior |
|---|---|---|
| Disconnected | IDLE |
Display off, deep sleep, minimal power |
session_start |
ACTIVE |
Wake animation, status indicator on |
status: thinking |
THINKING |
Slow pulse or animation |
status: tool |
WORKING |
Active animation showing tool name |
permission_request |
WAITING |
Impatient animation, button prompt |
session_stop |
IDLE |
Sleep animation, dim display |
The "visibly impatient" behavior in the reference implementation is a fast, agitated animation when a permission request is pending — a natural ambient cue that something needs your attention without requiring a notification.
Step 4: Add Custom Animations
The firmware supports custom GIF-based character animations per state. The workflow:
- Prepare your GIF frames and convert them to the device's animation format (conversion tooling is in the repo)
- Upload to the device via USB serial or OTA
- Map animation IDs to state machine transitions in your firmware
This is where the maker angle becomes compelling — the desk pet becomes a custom object with your own aesthetic, not just a generic status widget.
How to Verify the Connection Is Working
- Enable Developer Mode in Claude desktop and confirm the NUS service appears in nRF Connect
- Flash your firmware and power on the device
- The device should scan, find, and connect to Claude's service automatically on boot
- Start a Claude Code session — the device should transition from
IDLEtoACTIVE - Send Claude a task that triggers a bash command (e.g.,
run ls -la) - Confirm the permission prompt event arrives on the device and triggers the waiting animation
- Press approve on the device — Claude should proceed with the command
If step 4 fails, confirm with nRF Connect that the NUS service UUID is visible before the device tries to connect.
Troubleshooting Common Issues
Device does not find Claude's BLE service during scan: Developer Mode is likely not enabled or Claude lacks Bluetooth permission at the OS level. On macOS: System Settings → Privacy & Security → Bluetooth — add Claude to the allowed list. Verify the NUS UUID appears in nRF Connect independently before blaming the firmware.
Events arrive corrupted or partially parsed:
NUS frames can split across multiple BLE notifications — always buffer incoming bytes and wait for the newline terminator before attempting JSON deserialization. The incomingBuffer pattern in the sketch above handles this correctly.
Permission approval writes to the wrong characteristic: NUS naming is from the host's perspective. The characteristic labeled "RX" is the one Claude receives on — which means your device writes to it. Writing to TX instead silently fails. Double-check your characteristic handle before debugging the JSON.
Connection drops after a few minutes: Some BLE stacks negotiate aggressive connection intervals that Claude's desktop client doesn't tolerate. Increase the minimum connection interval in your NimBLE config and force a slower supervision timeout.
Approval response not acknowledged:
Confirm toolUseID in your response matches exactly what arrived in the permission_request event — including case. Any mismatch causes the response to be silently ignored.
Software Alternative: When You Don't Want to Build Hardware
The BLE maker API solves one specific problem well: an ambient desk display for Claude sessions when you are physically present at your desk. When you leave your desk, the ESP32 stays behind.
For developers whose primary friction is handling Claude Code notifications and permission prompts while away from their workspace — commute, meeting, different room — a software layer addresses that directly.
Grass runs Claude Code on an always-on cloud VM and forwards permission prompts to your phone via a native mobile app. The same approve/deny interaction the ESP32 desk pet gives you at your desk, Grass gives you anywhere with a data connection. The underlying mechanics are different (HTTP + SSE rather than BLE), but the workflow — see a permission request, tap approve, agent proceeds — is identical.
The honest comparison:
| Approach | Where It Works | Setup Time | Hardware Cost | Remote Access |
|---|---|---|---|---|
| claude-desktop-buddy (ESP32) | At your desk | 2–3 hours | ~$10–30 | No |
| claude-desktop-buddy (M5StickC Plus) | At your desk | 2–3 hours | ~$15 | No |
| Grass (cloud VM + mobile) | Anywhere | ~15 minutes | None | Yes |
They are not mutually exclusive. The BLE API is a local device integration; Grass runs in the cloud. Some developers will want both — a physical desk indicator when present, mobile access when not. They don't interfere with each other.
If the remote access angle is what you're evaluating, Grass vs Claude Code Remote Control breaks down the software options directly.
FAQ
What hardware does the claude-desktop-buddy BLE API support?
The official reference implementations use ESP32 and M5StickC Plus. Any microcontroller with BLE and Nordic UART Service support should work — the protocol is standard BLE using well-documented NUS UUIDs, not a proprietary transport. Check REFERENCE.md for the authoritative UUID list and JSON schemas before writing firmware.
Do I need an Anthropic API key to use the BLE maker API? No. The BLE connection is between Claude desktop (running on your machine) and your hardware device — it does not involve Anthropic's cloud API. Your device connects to the desktop app locally over Bluetooth. No API key, no registration, no network required for the BLE link itself.
Is the BLE maker API officially supported by Anthropic? No. Per the official repo, it is a developer feature requiring manual activation and is explicitly not officially supported. Treat it as a stable-enough maker experiment, not a production integration surface. The wire protocol could change without notice.
Can the device auto-approve permission prompts without human input? The protocol supports it — your firmware can write an approval response to the RX characteristic programmatically without waiting for button input. Whether to do this is a judgment call. Automated approval removes the oversight that permission prompts are designed to provide. If you do implement it, log every auto-approved action.
Does claude-desktop-buddy work with the Claude Code CLI (@anthropic-ai/claude-code), or only the desktop app?
The BLE API is specific to Claude desktop (macOS and Windows GUI app), not the Claude Code CLI. The CLI has a separate architecture and does not expose a BLE service. Grass and similar tools use the CLI via HTTP + SSE, which is a completely separate integration path.
Next Steps
- Read REFERENCE.md first — clone the claude-desktop-buddy repo and read the wire protocol before writing a line of firmware. The JSON schemas are the authoritative spec.
- Watch the walkthrough — the community demo on YouTube shows the ESP32 desk pet in action; useful for calibrating what "done" looks like before you start.
- Start with M5StickC Plus — if you're new to BLE firmware, the M5StickC Plus example has better display/button hardware out of the box and reduces the number of variables in your first build.
- For remote access — if the hardware approach isn't the right fit, Grass runs your agent on a persistent cloud VM with mobile permission forwarding. Free tier, no credit card.