Claude Code is good at a lot of things, but it has one habit (when not in auto mode) that can be incredibly frustrating for users. The moment it wants to run a command or edit a file outside what you've already allowed, it stops and waits for you to say yes. That's the correct, safe behavior, but the problem is that it waits silently. I've experienced this when using it wth my local LLM; I've tabbed away to read something or answer a message, the agent just sits there doing nothing, and I don't find out until I wander back to the window five minutes later and realize it's been idle the whole time.
So, when Anthropic quietly open-sourced a little firmware project called claude-desktop-buddy, I decided to take a look. It's a reference build for a physical desk pet that mirrors what Claude is doing in real time over Bluetooth, gets visibly impatient when a permission prompt is pending, and lets you approve or deny it by pressing a button on the device itself. The catch is that it's written for an M5StickC Plus, and I don't own one. But I do own a WT32-SC01 Plus, which is a board powered by the ESP32-S3 and packing a bigger screen.
For a fun project, I decided to port it. It's a bit rough in places, and the board is missing some hardware the original assumes, but it works. It sits on my desk, connects to the Claude app on my Mac, and tells me to come back the instant Claude needs me.
Interested in more maker-related content? We launched the XDA Maker Weekly newsletter featuring unique and original content you won't find anywhere else on XDA. Get subscribed by modifying your newsletter preferences!
Anthropic open-sourced a desk pet for exactly this problem
The bridge is local, and it stays off until you turn it on
The buddy isn't an official product, and Anthropic is pretty clear about that. To even connect it, you need to enable Developer Mode in the Claude desktop app. The project itself is MIT-licensed and aimed squarely at makers who want to tinker. When enabled, it exposes a local Bluetooth Low Energy link that any small ESP32 board can talk to. There's no API key and nothing routes through the internet; your computer and the device just talk directly. You can use it by going into Help, then enable Developer Mode, and in the new developer menu bar item, open the Hardware Buddy window.
Underneath, it's simple. The desktop app advertises a Nordic UART Service and streams newline-delimited JSON over it. Every snapshot carries how many sessions are running, how many are waiting, a one-line summary, your token counts, and, when Claude needs permission, a prompt object with the tool name and a hint about what it wants to do. That prompt field is the whole point. When it shows up, the device switches into its attention state.
The firmware itself is a small state machine with seven animations. There's sleep when nothing's connected, idle when Claude is connected but quiet, busy when sessions are running, and attention when a prompt is pending, which is the one that matters. There's also celebrate when you cross a token milestone, plus a couple of motion-driven ones for shaking the device or laying it face down. When a permission request arrives, the pet animates with urgency and an LED blinks, and you tap a button to send {"decision":"once"} or {"decision":"deny"} back over the same link. The ID on your response has to match the prompt exactly, which keeps you from accidentally approving the wrong thing.
To be clear about what this connects to, it's the Claude desktop app on macOS and Windows, not the terminal "claude" you might run in a shell. The Code and Cowork sessions inside the desktop app are what feed the bridge, and doing it through the terminal requires using Claude Code's Hooks and a different project altogether.
The WT32-SC01 Plus isn't on the supported list
No buttons, a bigger screen, and no motion sensor
The WT32-SC01 Plus is a very different board from the M5StickC Plus the buddy targets. It's an ESP32-S3 with a 3.5-inch 480×320 IPS panel driven over an 8-bit parallel bus, capacitive touch, and BLE 5. It's a nicer screen, and the parallel bus pushes pixels faster than the SPI display on the original. The trouble is that almost nothing about its hardware matches what the firmware expects.
The original has two physical buttons for approve and deny, an IMU for the shake and face-down gestures, a battery gauge, and a tiny 135×240 portrait display. My board has none of the buttons, no IMU, no fuel gauge, and a much larger screen typically used in landscape. On paper, that looks like a from-scratch rewrite, since the firmware assumes a screen and a set of buttons my board doesn't have.
In practice, I was surprised to discover that only two parts of the firmware care about any of that: the display layer and the input layer. The Bluetooth bridge, the JSON parsing, the state machine, the 18 ASCII pets, and the stats never touch the hardware directly, so they don't know or care which board they're running on. Anthropic kept that boundary clean, likely on purpose, so all the porting work happens in those two layers and nowhere else.
A single fake header handled most of it
Anthropic built this in layers
Every source file in the firmware starts with #include
This works because the firmware only ever uses a small piece of the M5's API, spread across approximately 30 calls. The shim handles each one and redirects it to the right hardware on my board. For example, M5.Lcd is still a real display object, it's just driving the ST7796 panel instead. The IMU, meanwhile, is a stub that reports the device isn't moving. Even the battery readings get faked, and I also disabled buzzer. All of this works because the M5StickC's display library is itself a fork of the same TFT_eSPI library that already supports my board, so every drawString and pushSprite call in the firmware was already configured correctly, it's just that it needed to be pointed at the right panel.
With display and hardware handled, next up were the buttons. This, also, turned out to be pretty easy. M5's device has physical buttons, but I also have a bigger screen here, so I split the bottom strip of the screen into two big touch zones and wired them to drive the same A and B buttons the firmware already expects. The original UI renders into the top of the panel, scaled up roughly 2x, and prints its own context hint right above the bar, and the two targets are always labelled by whatever the screen is showing. For example, on a permission prompt they read approve and deny, and in a menu, they read next and select. You simply tap left to approve, right to deny, and long-press the left button for the menu. The touch zones feed back into the existing button logic, so none of the behavior had to be rewritten.
In the end, nothing in the original firmware had changed. Split across five lines, the only edits to the application were:
- Scaling the UI up
- A touch tracker instead of a button tracker
- Button invalidation
- A touch bar draw
Managing everything else required just four additional files, for a total of about 260 new lines. For a board the project has never seen, moving the whole thing over came down to a simple shim and a few lines to wire it up.
The display gave me more trouble than the code
It was hard to get working right at first
Compiling clean isn't the same as working right away, and the display gave me the most trouble, though it was still fairly clean end to end. The first thing to go wrong was the toolchain, as the build defaulted to the newer Arduino-ESP32 core, which rewrote the Bluetooth classes and removed a couple of macros the firmware relies on. This meant it wouldn't compile at all. Pinning the platform back to the version the upstream targets fixed both problems, though.
A blank screen took longer to track down at first, as I could clearly see that the board powered on, the firmware booted fine, and the backlight came on. However, the panel showed nothing. As it turns out, the mainline TFT_eSPI library assumes all eight data pins of the parallel bus live in the same GPIO bank, and it works out which bank from the first data pin. My board puts most of its data pins in one bank but D1 on GPIO46, which is in the other. This caused the library to compute the wrong bit to toggle, never switch on D1 at all, and the panel would light up but stay empty. I ended up fixing it by swapping in a fork of TFT_eSPI built for this exact board, and it's one that drives both banks. It keeps the same class names, so the firmware stayed untouched.
Save on maker deals: microcontrollers, displays & gear
A fresh board also has no filesystem image, so the firmware printed a mount error. I wrote a small script so flashing the filesystem now bundles a character automatically. After that, the panel lit up properly and the pet appeared.
What it's like to actually use
The Cat pet does exactly what I wanted
I'm running the Cat pet, one of the 18 built-in ASCII species, and it works exactly as it should. When Claude is working, the cat looks busy. When a session finishes and everything's quiet, it settles into idle. And when Claude wants permission to run something, it sits up, shows me what it's asking for, and I can approve it with a tap without alt-tabbing back to the app. I now have a cat on my desk demanding attention if I ever opt to use the Claude application for tasks requiring permission prompts. It's pretty neat, and you can find it on my GitHub.
Sadly, there are some features that can't work with the stock configuration. With no IMU, the shake-to-dizzy and face-down-to-nap gestures have nothing to trigger them, so they're dormant, though the states themselves still fire from Bluetooth events. The attention LED toggles a pin that has nothing wired to it on my board, either. None of that touches the core loop which is currently working, though. The approve-and-deny flow, the live status, the token counts, and the pet all work, and that's the point of the whole project.
Honestly, I hesitate to refer to this as a "port." Anthropic built this in a clever way; by building the Bluetooth protocol in a separate layer, it doesn't matter what board you run this on. It feeds information to anything that asks for it, so swapping out what asks for it is rather trivial. Moving to a board with a bigger screen, a different display bus, no buttons, and no motion sensor came down to writing names for new hardware and adding a handful of lines. The work that actually took time was figuring out the GPIO bank issue, not the project itself.
If you just want the buddy working with the least possible effort, buy the M5StickC Plus it's written for and flash it. I went the harder way because I wanted the nicer screen and because I had the board sitting in a drawer that I could test it on. What I've got now is a desk pet wired into Claude Code, and the second it needs me, it makes sure I know.
