<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://vishalan.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://vishalan.com/" rel="alternate" type="text/html" /><updated>2026-08-11T05:36:36+00:00</updated><id>https://vishalan.com/feed.xml</id><title type="html">Vishalan Gharat</title><subtitle>Thoughts on engineering, AI, and building things.</subtitle><entry><title type="html">A Desk Pet That Approves My Code</title><link href="https://vishalan.com/blog/a-desk-pet-that-approves-my-code/" rel="alternate" type="text/html" title="A Desk Pet That Approves My Code" /><published>2026-07-23T15:30:00+00:00</published><updated>2026-07-23T15:30:00+00:00</updated><id>https://vishalan.com/blog/a-desk-pet-that-approves-my-code</id><content type="html" xml:base="https://vishalan.com/blog/a-desk-pet-that-approves-my-code/"><![CDATA[<figure class="post-media post-hero">
  <img src="/img/blog/buddy-approval.jpg" alt="A CYD touchscreen showing an alert ASCII capybara, an approval prompt, and HOLD-yes / TAP-no hints in the side bands" />
  <figcaption>Claude wants to run a command. The capybara is not thrilled about the wait.</figcaption>
</figure>

<p>My terminal asked for permission to run a command last night, and I answered it by pressing on a small anxious capybara.</p>

<p>Let me back up.</p>

<p>Anthropic recently published <a href="https://github.com/anthropics/claude-desktop-buddy">claude-desktop-buddy</a>, a lovely little maker project: a BLE protocol that lets Claude on your desktop stream session state to hardware, plus reference firmware for a desk pet that sleeps when nothing is happening, sweats when sessions are running, gets visibly impatient when a permission prompt is waiting — and lets you approve or deny it from the device itself. The reference hardware is an M5StickC Plus, a slim little stick with two physical buttons and a 1.14-inch screen.</p>

<p>I didn’t have an M5StickC Plus. I had the other famous cheap ESP32 board: the ESP32-2432S028, which the community affectionately calls the Cheap Yellow Display. About $15, a 2.8-inch touchscreen, and a hobbyist ecosystem that has documented every solder joint on it. No buttons to speak of, no IMU, a resistive touch panel instead — a different animal wearing the same chip.</p>

<p>So the project became: port the buddy to the CYD, without forking myself into maintenance misery.</p>

<h2 id="fork-like-the-manual-tells-you-to">Fork like the manual tells you to</h2>

<p>The first pleasant surprise was upstream’s CONTRIBUTING.md, which says, almost verbatim, that the best contribution is a fork and that ports to other boards should be exactly that. It even declares which files are the stable core — the BLE bridge and the JSON protocol parser — a polite way of saying <em>keep your hands off these and future pulls will be painless.</em></p>

<p>That shaped the whole architecture. The port lives behind a single header swap: every upstream file that said <code class="language-plaintext highlighter-rouge">#include &lt;M5StickCPlus.h&gt;</code> now says <code class="language-plaintext highlighter-rouge">#include "board.h"</code>, which hands back the real M5 library on the original hardware and a compatibility facade on the CYD. The facade — one header, one source file — impersonates the entire <code class="language-plaintext highlighter-rouge">M5.*</code> API the firmware touches: the display, the two buttons, the IMU, the power chip, the RTC, the beeper.</p>

<p>Some of those impersonations were almost free. <code class="language-plaintext highlighter-rouge">M5.Lcd</code> turns out to be a subclass of an embedded TFT_eSPI fork, so upstream’s draw calls are <em>literally</em> the TFT_eSPI API and the real library slots straight in. The 4MB flash, the LittleFS partition for GIF characters, the NVS settings — identical between boards. The BLE bridge compiled byte-for-byte untouched and paired with Claude Desktop on the first attempt, encrypted and bonded.</p>

<p>Some needed actual thought. The M5’s screen is 135×240; the CYD’s is 240×320, and an ESP32 without PSRAM cannot afford a full-screen 16-bit framebuffer — the math says 153KB, the heap says no. The trick was a display viewport: the panel letterboxes the original 135×240 canvas dead-center, upstream’s hardcoded <code class="language-plaintext highlighter-rouge">pushSprite(0,0)</code> lands exactly where it should, and the leftover side bands turn out to be useful real estate (more on that in a second).</p>

<p>And one impersonation was a small act of theatre. The original triggers a <em>dizzy</em> animation when you shake it — accelerometer, shake detection, spiral eyes. The CYD sits bolted to a desk with no IMU. But the shake detector just reads acceleration and looks for a spike, so my facade’s fake IMU reports calm gravity forever… until you scribble on the pet with your finger, at which point it reports one glorious fake spike and the untouched upstream code concludes it has been shaken. The pet gets dizzy. Nobody upstream is any the wiser.</p>

<figure class="post-media media-pair">
  <div class="pair-grid">
    <img src="/img/blog/cyd-board.jpg" alt="The bare ESP32-2432S028 board, yellow PCB with ESP32 module, microSD slot and dual USB ports" />
    <img src="/img/blog/buddy-first-boot.jpg" alt="The CYD showing the ASCII capybara idling on a dark screen with 'No Claude connected'" />
  </div>
  <figcaption>The $15 board, and first boot &mdash; capybara idling, no Claude in sight.</figcaption>
</figure>

<h2 id="touch-is-not-a-button">Touch is not a button</h2>

<p>The M5 original has two physical buttons: A approves, B denies. The obvious touch translation — tap left half to approve, tap right half to deny — has a problem that a review pass caught before I wrote a line of firmware: <strong>a resistive panel registers any pressure.</strong> Wiping dust off the screen should not grant Claude permission to run a shell command. A physical button demands deliberate force; a naked tap zone demands nothing.</p>

<p>So approval is asymmetric on purpose. While a prompt is pending, <em>deny</em> stays a plain tap — a false “no” costs you a retry. <em>Approve</em> requires pressing and holding for half a second — a false “yes” costs whatever the command was about to do, so it has to be a gesture you cannot make by accident. And those letterbox side bands earn their keep: while a prompt is up, they light with a green <strong>HOLD yes</strong> and a red <strong>TAP no</strong>, so the invisible zones are only invisible when nothing is at stake.</p>

<p>The rest of the input mapping fell out naturally. Tap left to cycle screens, tap right to page through, hold to open the menu, and the board’s lone physical BOOT button inherits the power-button duties. The one genuinely humbling discovery: the panel’s community calibration values worked on my unit unmodified, which by clone-hardware standards is a small miracle.</p>

<h2 id="the-gap-and-the-second-wire">The gap, and the second wire</h2>

<p>Then I hit the limitation that turned an evening port into something actually mine.</p>

<p>The desktop app’s bridge forwards the sessions <em>it</em> manages. Start a Claude Code session from the app and its permission prompts appear on the buddy, LED blinking, capybara agitated. Start one from a plain terminal — which is where I actually live — and the buddy dozes through it. The protocol is one-way; the device only knows what the desktop tells it, and the desktop doesn’t speak for terminals.</p>

<p>The fix came from two features that were clearly never designed to meet, and fit anyway.</p>

<p>First: the buddy firmware reads its JSON protocol on <em>two</em> transports — BLE and USB serial — and answers on both. That USB cable powering the pet is a fully functional command channel that nothing was using.</p>

<p>Second: Claude Code has a hook event called <code class="language-plaintext highlighter-rouge">PermissionRequest</code> that fires exactly when a permission dialog would appear, and lets an external command answer it.</p>

<p>Connect the dots and you get a forty-line bridge: the hook fires, a script writes the prompt down the USB cable, the capybara does its impatient dance, I press-and-hold, the decision travels back up the wire, and the hook returns <em>allow</em>. The terminal session proceeds as if I had answered it — because I did, just not with a keyboard. If the buddy is unplugged or I ignore it for 45 seconds, the normal terminal prompt appears and nothing is lost. The hook is inert unless the pet can answer.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Desktop-app sessions ── BLE ─────────────────┐
                                             ▼
Terminal sessions ── PermissionRequest hook ── USB serial ──► 🦫 decides
</code></pre></div></div>

<p>One wrinkle deserves a confession: both bridges run at once, and the desktop’s heartbeat kept clearing my serially-injected prompt every ten seconds mid-approval — the screen would flash the prompt, blank it, flash it again like a haunted jukebox. The fix is a dozen gated lines of arbitration in the firmware: a prompt that arrived over USB survives BLE’s attempts to clear it until it’s actually decided. The two transports now coexist without stepping on each other.</p>

<h2 id="the-parts-list-honestly">The parts list, honestly</h2>

<ul>
  <li><strong>ESP32-2432S028 “Cheap Yellow Display”</strong> — about $15. Get a USB-A-to-C cable; the board’s USB-C port is missing its CC resistors and a C-to-C cable will silently power nothing, which cost me exactly one confused minute.</li>
  <li><strong>Anthropic’s claude-desktop-buddy</strong> — MIT licensed, fork-friendly by design.</li>
  <li><strong>Claude Desktop</strong> with Developer Mode for the BLE side; a <code class="language-plaintext highlighter-rouge">PermissionRequest</code> hook for the terminal side.</li>
  <li>One evening, most of which was spent on research and review before any code — which is why the code mostly worked when it finally existed.</li>
</ul>

<p>The fork, including the CYD board target, the touch input layer, the terminal bridge, and a one-command hook installer, is on my GitHub. Flash it with <code class="language-plaintext highlighter-rouge">pio run -e cyd2usb -t upload</code>, pair it from Claude Desktop’s Developer menu, run <code class="language-plaintext highlighter-rouge">./tools/install_hook.sh</code>, and a small creature on your desk becomes the arbiter of what your AI is allowed to execute.</p>

<p>There is something quietly correct about the ergonomics. A permission prompt is a real decision, and moving it off the screen — to a physical object, with a deliberate gesture, guarded by a pet that judges you — makes it feel like one again. Also the capybara does a little heart animation when you approve quickly, and I am not above admitting that this modifies my behavior.</p>

<figure class="post-media">
  <img src="/img/blog/buddy-info-page.jpg" alt="The buddy's info screen showing session counts and an encrypted BLE link" />
  <figcaption>The link page: sessions tracked, BLE encrypted, last message seconds ago.</figcaption>
</figure>]]></content><author><name>Vishalan Gharat</name></author><category term="engineering" /><category term="esp32" /><category term="claude" /><category term="claude-code" /><category term="hardware" /><category term="maker" /><category term="cyd" /><summary type="html"><![CDATA[Anthropic's claude-desktop-buddy runs on an M5StickC Plus. I had a Cheap Yellow Display instead. One evening later: a capybara on my desk that blinks when Claude wants permission, and approves it when I press on its screen.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://vishalan.com/img/blog/buddy-approval.jpg" /><media:content medium="image" url="https://vishalan.com/img/blog/buddy-approval.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Change Your Sky</title><link href="https://vishalan.com/blog/change-your-sky/" rel="alternate" type="text/html" title="Change Your Sky" /><published>2026-07-22T03:30:00+00:00</published><updated>2026-07-22T03:30:00+00:00</updated><id>https://vishalan.com/blog/change-your-sky</id><content type="html" xml:base="https://vishalan.com/blog/change-your-sky/"><![CDATA[<figure class="post-media post-hero">
  <video src="/img/blog/one-vanderbilt-timelapse.mp4" poster="/img/blog/one-vanderbilt-timelapse.jpg" autoplay="" loop="" muted="" playsinline=""></video>
  <figcaption>The sky changing over Manhattan, from the top of One Vanderbilt.</figcaption>
</figure>

<p>Almost two thousand years ago, Seneca wrote to a friend who kept travelling to outrun his own unhappiness and kept arriving as the same unhappy man. His verdict was blunt. You must change your soul, not your sky. Wherever you go, he said, you take yourself with you. He was borrowing from Horace, who put it even more plainly: people who race across the sea change their sky, not their state of mind.</p>

<p>I kept thinking about that line on this trip, because on paper I was doing exactly the thing Seneca warned against. Six years without a proper break, and then two and a half weeks on the far side of the planet, following a World Cup across America. A textbook case of changing the sky and hoping the rest would sort itself out.</p>

<p>Except it did sort itself out. And the reason, I think, comes down to a distinction almost nobody bothers to make: the difference between the two kinds of time off.</p>

<p>There is the ad-hoc break. The long weekend, the week grabbed between deadlines. You know the shape of it. You spend the first half still mentally at your desk and the second half quietly dreading the return. Nothing gets built in that time. It is a blank in the real business of working, and you treat it as one.</p>

<p>The Mahabharata offers the opposite picture of what a pause can be. When the Pandavas lost everything in the rigged game of dice, they were sent into the forest for thirteen years, and the Vana Parva, the Book of the Forest, could easily have been a chronicle of punishment and waiting. It is the opposite. Out there, stripped of the palace and everything that came with it, Arjuna walked north into the Himalayas and sat in penance so severe the earth itself is said to have smoked, until Shiva stood before him and he came away with weapons no ordinary warrior could hold. His brothers sought out the sages who lived beyond the reach of the court and gathered what those men knew. The forest was not the absence of the kingdom. It was where five princes were quietly forged into the people who could win it back. The most important work of their lives happened during what looked, from the outside, like time off.</p>

<p>Most of my breaks across six years had been the first kind. Maintenance. Downtime for the machine so the machine could run again on Monday. Not once had I gone to the forest.</p>

<p>So this time I planned for something else. The planning is the part that sounds least profound and turns out to matter most. It had to feel like a genuine holiday and clear a few things off the bucket list, but it also had to keep me sharp. I did not want to come home rested and dull. I wanted to come home changed.</p>

<p>The holiday half was easy to fall for. The anchor was the World Cup, and it delivered from the very first match I saw: Argentina against Egypt in the Round of 16, one of the most entertaining games of the whole tournament, three goals to two, a match that had no business being that tense, eighty thousand people on their feet at once. Around the football were the ordinary large things you save up to see. Times Square at midnight with every screen synced. A two-hour timelapse from the top of One Vanderbilt that is still the best thing on my phone. Fireworks over Washington on the Fourth of July, running an hour late and worth the wait. Bucket-list stuff, taken slowly instead of rushed through.</p>

<figure class="post-media media-pair">
  <div class="pair-grid">
    <video src="/img/blog/dc-fireworks.mp4" poster="/img/blog/dc-fireworks.jpg" autoplay="" loop="" muted="" playsinline=""></video>
    <img src="/img/blog/dc-fireworks-selfie.jpg" alt="A selfie in the Fourth of July crowd with fireworks bursting over the water behind" />
  </div>
  <figcaption>Fireworks over Washington on the Fourth of July &mdash; an hour late, and worth the wait.</figcaption>
</figure>

<p>But a mind left idle on a beach comes home soft, and soft was the one thing I was trying to avoid. So I kept both sets of muscles working. The physical side mostly took care of itself: long drives across states, early runs on the sand, miles on foot through cities, a bike along an empty National Mall at dawn to beat the heat. The mental side I had to be deliberate about. I read. I wrote something most nights, even when it was three tired lines. And I let the trip hand me real problems to solve, the kind that do not wait for a convenient moment: plans that collapsed and had to be rebuilt from a crowded terminal, connections remade on the fly, decisions taken with no time to sit on them. Stressful in the moment, but exactly the sort of live problem that keeps a mind switched on. The aim was never productivity. It was to stay awake to the trip, present enough to actually take it in. The Pandavas did not idle away their exile; they walked, they trained, they went looking for what they did not yet know. A real pause is active in that same way, and I wanted mine to be.</p>

<figure class="post-media">
  <img src="/img/blog/food-collage.jpg" alt="A ten-photo grid of food from the trip: cheeseburger, oysters, Philly cheesesteak, Old Bay blue crab, butter-chicken tacos, fried calamari, steak, mussels, a taco bowl, and soft-shell crab with hush puppies" />
  <figcaption>Being present, one plate at a time &mdash; Old Bay blue crab, Gulf oysters, a Philly cheesesteak, and everything the coast would fry.</figcaption>
</figure>

<p>Here is where I part ways with Seneca, or perhaps where I finally understood him. He is right that travel cannot fix a man who goes in order to escape, because escape keeps your attention pointed backward at the thing you are fleeing. But if you go to look rather than to flee, a new sky does something the old one cannot. It strips away the automatic. At home your routine runs you on rails you have long stopped noticing. Set yourself down somewhere unfamiliar and every ordinary act becomes a small decision again, and in the gap those decisions open up, you can finally see the shape of the life you left behind. You do not change because the sky changed. You change because the sky stops hiding you from yourself.</p>

<p>The trip kept proving this through people. On my worst travel day, when the flight out of Washington was delayed six hours and then cancelled outright, a stranger named Meywin, an Indian who had moved to America years earlier, stepped in. He did not offer sympathy so much as help me think. While the airline came apart around us, he worked the problem with me, and together we booked onto the most expensive flight of the entire trip for the next morning, the one thing that got me to that Argentina and Egypt match with barely time to make the stadium. He drove me to the airport himself, the way you would for family. The next day I met Faris in the same wreckage of stranded passengers and gave him a ride hours down the road. You brace for a trip like this to be lonely. Mostly it was a steady relay of strangers arriving exactly when I needed them, and each one was a small correction to the story I tell myself about my own self-sufficiency.</p>

<figure class="post-media media-pair">
  <div class="pair-grid">
    <img src="/img/blog/arg-vs-egypt.jpg" alt="With a friend in the stands for Argentina against Egypt, the pitch below dressed in both flags" />
    <img src="/img/blog/arg-vs-england.jpg" alt="With a friend in the stands at another Argentina match" />
  </div>
  <figcaption>Matchdays are better with company &mdash; the games that anchored the trip, shared with a friend.</figcaption>
</figure>

<p>It all settled into focus at Kitty Hawk, on the strip of Outer Banks sand where the Wright brothers first left the ground. Twelve seconds, a hundred feet. What strikes you standing there is not the daring but the modesty of it. Nobody ever changed the world by fleeing somewhere. They changed it by paying close, patient attention to one stubborn problem until it lifted off the ground. Which is more or less what a real pause is for.</p>

<figure class="post-media media-pair">
  <div class="pair-grid">
    <img src="/img/blog/outer-banks-beach.jpg" alt="Sitting under a beach umbrella on the Outer Banks sand, looking out at the Atlantic" />
    <video src="/img/blog/outer-banks-beach.mp4" poster="/img/blog/outer-banks-beach-poster.jpg" autoplay="" loop="" muted="" playsinline=""></video>
  </div>
  <figcaption>The Outer Banks, where it all settled into focus.</figcaption>
</figure>

<p>By the end, the World Cup had quietly become the spine of the trip. I had followed Argentina from the round of sixteen in Atlanta, through the semi-final against England, all the way to the final at MetLife. Spain won, and none of it felt sour, because I was there for Messi’s last ever World Cup. It takes a generation to witness a player like him, and he had already had his happy ending in Qatar, the kind most people would retire on. He came back for one more chapter he owed no one. There is a particular dignity in watching someone great choose his own moment to stop, while the watching is still worth doing. I had spent six years not stopping. He was a reminder that knowing when to pause is not the opposite of ambition. It may be its most advanced form.</p>

<figure class="post-media media-collage">
  <div class="collage-grid">
    <img class="collage-main" src="/img/blog/worldcup-final.jpg" alt="At the World Cup final at MetLife Stadium, the Argentina and Spain flags unfurled across the pitch behind" />
    <div class="collage-side">
      <video src="/img/blog/worldcup-final-clip.mp4" poster="/img/blog/worldcup-final-clip-poster.jpg" autoplay="" loop="" muted="" playsinline=""></video>
      <img src="/img/blog/worldcup-trophy.jpg" alt="The FIFA World Cup 2026 trophy on the podium at MetLife Stadium, the Spain flag across the pitch and the stands packed beyond" />
    </div>
  </div>
  <figcaption>The final at MetLife. Spain lifted the trophy, and Messi's last ever World Cup.</figcaption>
</figure>

<p>Seneca was half right. A change of sky will not save a man who travels to run away. But for the one who travels to wake up, the sky was never the point. It is only the thing that finally gets you to change your soul.</p>

<p>It had been six years since I last looked up. I don’t plan to leave it that long again.</p>]]></content><author><name>Vishalan Gharat</name></author><category term="travel" /><category term="travel" /><category term="reflection" /><category term="world-cup" /><category term="stoicism" /><category term="mahabharata" /><summary type="html"><![CDATA[Six years without a real break, then a World Cup trip across America. What Seneca and the Mahabharata say about the two kinds of time off, and why a real pause wakes you up.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://vishalan.com/img/blog/worldcup-final-og.jpg" /><media:content medium="image" url="https://vishalan.com/img/blog/worldcup-final-og.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>