<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/"
  xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Agnel Nieves - Side Projects</title>
    <link>https://agnelnieves.com/blog/tag/side-projects</link>
    <description>Blog posts on Side Projects by Agnel Nieves.</description>
    <language>en-US</language>
    <lastBuildDate>Fri, 15 May 2026 01:12:12 GMT</lastBuildDate>
    <atom:link href="https://agnelnieves.com/blog/tag/side-projects/feed.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title><![CDATA[Building a Terminal Portfolio You Can SSH Into]]></title>
      <link>https://agnelnieves.com/blog/building-a-terminal-portfolio-you-can-ssh-into</link>
      <guid isPermaLink="true">https://agnelnieves.com/blog/building-a-terminal-portfolio-you-can-ssh-into</guid>
      <description><![CDATA[How I shipped ssh agnelnieves.sh, a terminal portfolio in Rust using ratatui, russh, and Fly.io. One binary, two modes: local CLI and SSH server.]]></description>
      <content:encoded><![CDATA[<h2>TL;DR</h2>
<p>You can now browse this site in your terminal:</p>
<pre><code class="language-bash">ssh agnelnieves.sh
</code></pre>
<p>No install. One Rust binary that runs as either a local CLI or an in-binary SSH server. No system sshd, no user accounts, no auth. The TUI is built with <a href="https://ratatui.rs">ratatui</a>; the SSH layer is <a href="https://docs.rs/russh">russh</a>; render code is shared between local and remote modes. It&#39;s hosted on Fly.io behind a dedicated IPv4, and the whole thing costs about $2/mo. The code lives as a <code>cli/</code> package inside the same Next.js project that powers this site, so the website and the terminal share a single deploy story. If you want the full build (architecture, code, Fly.io setup), I wrote it up as a <a href="/guides/ssh-terminal-portfolio.md">step-by-step guide</a>.</p>
<h2>The setup</h2>
<p><code>ssh agnelnieves.sh</code>. That&#39;s it. No <code>-l user@</code>, no keys to add, no first-time signup. The server accepts whatever username your local SSH client offers and drops you straight into a terminal UI.</p>
<p>Press <code>h</code>/<code>a</code>/<code>b</code>/<code>c</code> to jump between Home, About, Blog, and Connect. <code>j</code>/<code>k</code> (or arrows) to navigate lists. <code>Enter</code> to open. <code>q</code> to quit, which closes the SSH session.</p>
<h2>Why I built it</h2>
<p>Honestly? I saw <a href="https://terminal.shop">terminal.shop</a> and thought it was awesome. They sell coffee at <code>ssh terminal.shop</code> (that&#39;s the whole storefront, no website) and the first time I tried it I wanted to know if I could build something like it.</p>
<p>I do side projects like this when I need to step away from whatever I&#39;m shipping day-to-day. They&#39;re not pitches. They don&#39;t need a thesis. They&#39;re more like the way some people fix a bike on a Saturday: sit down, follow whatever&#39;s pulling on your curiosity, and at the end you have a thing that didn&#39;t exist before. That&#39;s the whole reason.</p>
<p>So this was that. A few evenings poking at <a href="https://ratatui.rs">ratatui</a> and <a href="https://docs.rs/russh">russh</a> to see what it would take to make <code>ssh agnelnieves.sh</code> actually work. Turns out: less than I expected. The hardest parts were the ones I didn&#39;t see coming, which is what makes any of this fun.</p>
<h2>The architecture</h2>
<p>One binary. Two modes. One render path.</p>
<pre><code>            ┌─────────────────────────────┐
            │     agnel  (single .exe)    │
            └──────────────┬──────────────┘
                           │
            ┌──────────────┴──────────────┐
            │                             │
       agnel (no flag)             agnel --serve
       Local TUI                   SSH server (Fly.io)
       crossterm stdin             russh + custom backend
            │                             │
            └──────────┬──────────────────┘
                       │
                src/render.rs   ← shared
                       │
                Per-session App state
                       │
                Live fetches from agnelnieves.com APIs
</code></pre>
<p>The pieces:</p>
<ul>
<li><strong>TUI:</strong> <a href="https://ratatui.rs">ratatui</a> draws everything: header, ticker, ASCII banner, projects list, blog reader. The render function lives in <code>src/render.rs</code> and is identical whether the binary is running on your laptop or whether you&#39;re seeing it over SSH.</li>
<li><strong>State machine:</strong> <code>App</code> in <code>src/app.rs</code> owns screen, scroll position, list selection, ticker animation, and async-fetch results funneled through an <code>mpsc::Receiver</code>.</li>
<li><strong>Data:</strong> zero content is bundled. The CLI fetches live from <code>/feed.json</code>, <code>/api/blog/[slug]/raw</code>, <code>/api/projects</code>, and <code>/api/site.json</code> on this site, cached for an hour at <code>~/.agnel/cache/</code>. So a new blog post here shows up in your terminal immediately without redeploying anything.</li>
<li><strong>SSH server:</strong> <a href="https://docs.rs/russh">russh 0.60</a> in <code>src/serve.rs</code>. One tokio task per session.</li>
</ul>
<h2>The decision that made it easy: don&#39;t fork a PTY</h2>
<p>Most &quot;expose a TUI over SSH&quot; guides suggest spawning a pseudo-terminal and <code>exec</code>-ing the TUI binary inside it. That&#39;s how <code>mosh</code>, <code>tmux</code>, and Go&#39;s <a href="https://charm.sh/blog/wish/"><code>wish</code></a> library (the one terminal.shop uses) handle things. It works, but it&#39;s heavier:</p>
<ul>
<li>Adds a libc dependency (<code>forkpty</code>)</li>
<li>Forks a process per connection (more memory, slower spawn)</li>
<li>Hard to share state between the SSH layer and the app</li>
</ul>
<p>Russh&#39;s <code>ratatui_app</code> example showed a cleaner path: implement a custom <code>std::io::Write</code> backend that funnels bytes through russh&#39;s <code>Handle::data(channel, bytes)</code> API. Ratatui doesn&#39;t know it&#39;s writing to an SSH channel instead of stdout. No fork, no PTY, just bytes:</p>
<pre><code class="language-rust">struct TerminalSink {
    sender: UnboundedSender&lt;Vec&lt;u8&gt;&gt;,
    sink: Vec&lt;u8&gt;,
}

impl std::io::Write for TerminalSink {
    fn write(&amp;mut self, buf: &amp;[u8]) -&gt; std::io::Result&lt;usize&gt; {
        self.sink.extend_from_slice(buf);
        Ok(buf.len())
    }
    fn flush(&amp;mut self) -&gt; std::io::Result&lt;()&gt; {
        if !self.sink.is_empty() {
            let _ = self.sender.send(std::mem::take(&amp;mut self.sink));
        }
        Ok(())
    }
}
</code></pre>
<p>A background tokio task drains the channel and pushes bytes back to the client through russh. Each session gets its own <code>App</code> and its own <code>Terminal&lt;CrosstermBackend&lt;TerminalSink&gt;&gt;</code>. The lock graph stays tiny: an <code>Arc&lt;Mutex&lt;SessionState&gt;&gt;</code> per session, plus a <code>HashMap</code> of all sessions for cleanup.</p>
<p>Inputs go the other direction. Russh hands raw bytes to my <code>data()</code> callback, and a small parser turns them into the same <code>crossterm::event::KeyEvent</code> values the local TUI uses. That meant zero changes to <code>App::handle_key</code>:</p>
<pre><code class="language-rust">fn parse_keys(buf: &amp;[u8]) -&gt; Vec&lt;KeyEvent&gt; {
    // 0x03 → Ctrl+C, 0x1b[A → Up, 0x1b alone → Esc,
    // printable ASCII → Char(b), etc.
}
</code></pre>
<p>Window resize hooks into the same place. <code>window_change_request</code> calls <code>terminal.resize(rect)</code>, and ratatui redraws on the next 50 ms tick.</p>
<p>The result: a single binary you can drop on a VPS, in a container, or even into a Lambda-style worker, and it just serves the TUI.</p>
<h2>What I learned about hosting</h2>
<p>A few things I didn&#39;t expect.</p>
<p><strong>1. Fly trial orgs block dedicated IPv4, and SSH needs IPv4.</strong> For HTTP, Fly&#39;s edge gives you a shared anycast IPv4 for free. For raw TCP on port 22, you need a dedicated v4 (~$2/mo), and trial orgs can&#39;t allocate one. Pure-IPv6 isn&#39;t a real option, either: most US residential ISPs still don&#39;t have native IPv6 SSH paths. Adding a card was the unlock.</p>
<p><strong>2. The SSH host key has to persist.</strong> If the container generates a fresh Ed25519 key on every boot, every returning visitor gets <code>REMOTE HOST IDENTIFICATION HAS CHANGED</code> warnings after the next deploy. The fix is a 1 GB Fly volume mounted at <code>/data</code>, with <code>--host-key /data/ssh_host_key</code>. First boot generates and writes the key; every boot after just reads it.</p>
<p><strong>3. Trial machines auto-stop after 5 minutes.</strong> That&#39;s how the Fly free tier nudges you toward billing. Once you&#39;re paying, set <code>auto_stop_machines = &quot;off&quot;</code> and <code>min_machines_running = 1</code> if you want a long-running service.</p>
<p><strong>4. <code>fly proxy</code> is fine for HTTP, weird for raw SSH.</strong> I burned half an hour on <code>Connection reset by peer</code> through <code>fly proxy 12222:2222</code> before realizing I should just <code>bash /dev/tcp/127.0.0.1/2222</code> from inside the machine via <code>flyctl ssh console</code>. The SSH server was healthy the whole time; the proxy path was the problem.</p>
<h2>ANSI Shadow beats slant</h2>
<p>The first banner I shipped used figlet&#39;s <code>slant</code> font. It looks fine in a docs file. At terminal width with full-block rendering, it falls apart into disconnected diagonals.</p>
<p>Swapped to <a href="https://patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=AGNEL%20NIEVES">ANSI Shadow</a>, the same boxy block font terminal.shop and <a href="https://charm.sh">charm.sh</a> use. Six rows tall, about 93 columns wide, reads instantly. Probably the kind of thing I&#39;d have gotten right the first time if I&#39;d looked at more references before opening figlet.</p>
<h2>Visitor #N</h2>
<p>One fun thing I added. Look at the bottom-left of the footer when you connect: it says <code>visitor #N</code>. First SSH session ever was #1, second was #2, and so on. The count lives in a tiny text file on the Fly volume next to the host key, increments once per <code>channel_open_session</code>, and persists across deploys (host-key lesson applied).</p>
<p>Implementation is about thirty lines. An <code>AtomicU64</code> loaded from the file at startup, a <code>spawn_blocking</code> write after each <code>fetch_add</code>. The file is just a base-10 number with no header, so <code>cat /data/visitor_count</code> is the dashboard.</p>
<p>There&#39;s no leaderboard. No real meaning to the number. But watching it tick up after shipping is the moment the whole side project starts feeling worth it.</p>
<h2>The stack</h2>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Tech</th>
</tr>
</thead>
<tbody><tr>
<td>TUI</td>
<td>ratatui 0.29, crossterm 0.28</td>
</tr>
<tr>
<td>SSH</td>
<td>russh 0.60</td>
</tr>
<tr>
<td>HTTP client</td>
<td>ureq 2 (sync, plenty for this)</td>
</tr>
<tr>
<td>Async runtime</td>
<td>tokio 1</td>
</tr>
<tr>
<td>CLI args</td>
<td>clap 4</td>
</tr>
<tr>
<td>Host</td>
<td>Fly.io (<code>iad</code>, shared-cpu-1x, 256 MB, 1 GB volume)</td>
</tr>
<tr>
<td>DNS</td>
<td>Vercel Domains (A + AAAA on the apex)</td>
</tr>
<tr>
<td>Binary size</td>
<td>~3 MB release (macOS arm64), 4.3 MB (Linux x86_64)</td>
</tr>
<tr>
<td>Image size</td>
<td>28 MB (Debian bookworm-slim runtime)</td>
</tr>
<tr>
<td>Cost</td>
<td>$2/mo for the dedicated IPv4 + free tier for the rest</td>
</tr>
</tbody></table>
<h2>What&#39;s next</h2>
<ul>
<li><strong>Pretty errors and loading states.</strong> Today they say <code>Loading…</code> which is functional but boring.</li>
<li><strong>Project deep-links.</strong> I render project metadata but not the original write-ups. Pulling MDX through the API would let me ship long-form case studies in-terminal.</li>
<li><strong>Cross-platform release binaries.</strong> The current install path is <code>cargo install --path cli/</code>. Homebrew tap + release artifacts for macOS and Linux is a Saturday morning.</li>
</ul>
<h2>Try it</h2>
<pre><code class="language-bash">ssh agnelnieves.sh
</code></pre>
<p>If you want to build your own version, the <a href="/guides/ssh-terminal-portfolio.md">guide</a> has the full setup (Rust + ratatui + russh + Fly.io), with a copy-paste prompt at the top so you can hand it to your AI agent of choice. If you ship one, <a href="/connect">let me know</a>.</p>
<hr>
<p><em>Built by <a href="/about">Agnel Nieves</a>, a design engineer with 15+ years across product, design systems, and crypto. More writing on <a href="/blog">the blog</a>.</em></p>
]]></content:encoded>
      <pubDate>Tue, 12 May 2026 00:00:00 GMT</pubDate>
      <author>agnel@agnelnieves.com (Agnel Nieves)</author>
      <dc:creator><![CDATA[Agnel Nieves]]></dc:creator>
      <category>Rust</category>
      <category>Ratatui</category>
      <category>SSH</category>
      <category>Fly.io</category>
      <category>Design Engineering</category>
      <category>Side Projects</category>
    </item>
    <item>
      <title><![CDATA[We Found a Hidden Pet System in Claude Code's Leaked Source and Shipped It Overnight]]></title>
      <link>https://agnelnieves.com/blog/we-found-a-hidden-pet-system-in-claude-codes-leaked-source-and-shipped-it-overnight</link>
      <guid isPermaLink="true">https://agnelnieves.com/blog/we-found-a-hidden-pet-system-in-claude-codes-leaked-source-and-shipped-it-overnight</guid>
      <description><![CDATA[Anthropic's Claude Code source leaked via npm. Buried inside: a pixel pet system called Buddy. We turned it into an open-source toy in a day.]]></description>
      <content:encoded><![CDATA[<p>On March 30th, 2026, Anthropic accidentally shipped a 59.8 MB source map file inside their Claude Code npm package. Within hours, the entire 512,000-line TypeScript codebase was mirrored across GitHub and picked apart by thousands of developers. Buried inside that code, alongside feature flags for autonomous agents and undercover commit modes, was something nobody expected: a fully built virtual pet system called <strong>Buddy</strong>.</p>
<p>My co-founder <a href="https://x.com/peronif5">peroni</a> and I did the only sensible thing. We shipped it.</p>
<TwitterEmbed>
<blockquote className="twitter-tweet" data-theme="dark"><p lang="en" dir="ltr">We all know what happened yday with Claude Code. Buried in the source: a hidden pet system called &quot;Buddy.&quot; Every user gets a unique pixel creature based on their ID. Deterministic, same hash, same buddy, every time. So <a href="https://twitter.com/peronif5?ref_src=twsrc%5Etfw">@peronif5</a> and I did the most sensible thing... Shipped it.… <a href="https://t.co/YE4ZSEIXq7">pic.twitter.com/YE4ZSEIXq7</a></p>&mdash; Agnel (🇵🇷) (@agnelnieves) <a href="https://twitter.com/agnelnieves/status/2039311800005525807?ref_src=twsrc%5Etfw">April 1, 2026</a></blockquote>
</TwitterEmbed><h2>What Happened with the Claude Code Leak</h2>
<p>If you missed it, here&#39;s the short version. Chaofan Shou (<a href="https://x.com/Fried_rice">@Fried_rice</a>) noticed that version 2.1.88 of the <code>@anthropic-ai/claude-code</code> package on npm included an unminified source map — <code>cli.js.map</code> — containing the full, readable TypeScript source. By 4:23 AM ET, it was public. By noon, the internet had catalogued every hidden feature, internal codename, and unreleased capability Anthropic had been quietly building.</p>
<p>Among the bigger discoveries:</p>
<ul>
<li><strong>KAIROS</strong> — an always-on daemon mode that lets Claude Code operate as a persistent background agent, watching, logging, and acting without waiting for user input</li>
<li><strong>Undercover mode</strong> — auto-activated for Anthropic employees on public repos, stripping AI attribution from commits</li>
<li><strong>44 feature flags</strong> covering unreleased functionality</li>
<li>And tucked away, a complete companion pet system called <strong>Buddy</strong></li>
</ul>
<h2>The Buddy System: A Pixel Pet for Every User</h2>
<p>The Buddy system was fully implemented. Every Claude Code user was supposed to get a unique pixel creature generated deterministically from their user ID. Same hash, same buddy, every time. The code included 18 species across rarity tiers, stat generation, personality descriptions — the whole gacha experience, just waiting to be turned on.</p>
<p>It was the kind of detail that makes you smile. In a tool built for productivity and code generation, someone at Anthropic took the time to build a pet system. A little pixel friend that lives in your terminal. That&#39;s the kind of craft and whimsy that makes developer tools memorable.</p>
<p>The moment I saw it, I knew what we had to do.</p>
<h2>From Discovery to Deploy in a Day</h2>
<p>Peroni and I have a rhythm. We spot something interesting, we build. No planning committee, no Jira tickets, no &quot;let&#39;s circle back Monday.&quot; As a design engineer with 15+ years of shipping products, I&#39;ve learned that the best side projects are the ones you can&#39;t <em>not</em> build. This was one of those.</p>
<p>We built <a href="https://www.claudebuddy.me/">Claude Buddy</a> — a web app that lets anyone generate their own pixel companion. Type your name or your Claude Code user ID, and watch it draw your buddy pixel by pixel, complete with retro CRT animations and pop sounds.</p>
<p>Here&#39;s what we shipped:</p>
<ul>
<li><strong>12 unique pixel art species</strong> across 5 rarity tiers — from common Blobbits to the legendary Nebulynx (3% chance)</li>
<li><strong>Deterministic generation</strong> — same name always produces the same buddy, making them feel like <em>yours</em></li>
<li><strong>Shiny variants</strong> with a 5% drop rate, because of course</li>
<li><strong>Buddy stats</strong> — Vibe, Chaos, Focus, and Luck, each randomly rolled but consistent to your hash</li>
<li><strong>One-command terminal install</strong> — <code>curl</code> a script and your buddy appears in your Claude Code statusline</li>
<li><strong>Social sharing</strong> — download as PNG, share via URL, generate QR codes, post to X/LinkedIn with pre-populated text</li>
</ul>
<p>The whole thing runs on Next.js 15, renders sprites on HTML5 Canvas, and uses a Mulberry32 PRNG seeded by a DJB2 hash of your input. No backend, no database, no authentication. Pure deterministic fun.</p>
<h2>Why Build This?</h2>
<p>Partly because it&#39;s fun. Partly because of <a href="/blog/ai-native-design-gap-from-static-to-dynamic-experiences">how I approach creative work</a> — I try to ship a hackathon-style project every quarter to stay sharp and experiment with new patterns. But mostly because I think the Buddy system represents something important about how we relate to our tools.</p>
<p>Developer tools don&#39;t have to be purely utilitarian. The best ones have personality. They reward curiosity. They make you <em>want</em> to open the terminal. A pixel pet that lives next to your cursor won&#39;t make you a better programmer, but it might make the work feel a little less solitary.</p>
<p>Anthropic clearly felt the same way — they built the whole thing, ready to ship. We just opened the door a little early.</p>
<h2>How the Generation Algorithm Works</h2>
<p>For the technically curious, the generation is straightforward:</p>
<ol>
<li>Take the input string (name or user ID), lowercase and trim it</li>
<li>Salt it with a fixed string to avoid collisions</li>
<li>Run a DJB2 hash to convert it to a numeric seed</li>
<li>Feed that seed into a Mulberry32 PRNG</li>
<li>Roll for species (weighted by rarity tier probabilities)</li>
<li>Roll for shiny status (5% chance)</li>
<li>Generate four stats between 1 and 99</li>
<li>Select a soul description from the species pool</li>
</ol>
<p>Same input, same seed, same rolls, same buddy. Every time. No server involved.</p>
<h2>It&#39;s Open Source</h2>
<p>The entire project is <a href="https://github.com/basement-browser/claude-buddy">open source on GitHub</a>. Built by the <a href="https://basementbrowser.com">Basement</a> team. Fork it, remix it, hatch your own buddy.</p>
<p>If you want to see what came out of this, check the <a href="https://x.com/agnelnieves/status/2039311800005525807">thread on X</a> where we announced it. And if you&#39;re building with Claude Code, maybe your buddy is already waiting — just type your name and find out.</p>
<hr>
<p><em>Have thoughts on this or want to collaborate on something weird? <a href="/connect">Let&#39;s connect</a>.</em></p>
]]></content:encoded>
      <pubDate>Wed, 01 Apr 2026 00:00:00 GMT</pubDate>
      <author>agnel@agnelnieves.com (Agnel Nieves)</author>
      <dc:creator><![CDATA[Agnel Nieves]]></dc:creator>
      <category>AI</category>
      <category>Claude Code</category>
      <category>Open Source</category>
      <category>Side Projects</category>
      <category>Creative Coding</category>
    </item>
  </channel>
</rss>