<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://tucker.wales/feed.xml" rel="self" type="application/atom+xml" /><link href="https://tucker.wales/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-06-23T13:26:25+00:00</updated><id>https://tucker.wales/feed.xml</id><title type="html">Joshua Tucker</title><subtitle>Software engineer based in Wales, building reliable backend and platform systems.</subtitle><author><name>Joshua Tucker</name></author><entry><title type="html">Introducing perq: Your Pull Requests, in the Terminal</title><link href="https://tucker.wales/writing/introducing-perq/" rel="alternate" type="text/html" title="Introducing perq: Your Pull Requests, in the Terminal" /><published>2026-06-23T00:00:00+00:00</published><updated>2026-06-23T00:00:00+00:00</updated><id>https://tucker.wales/writing/introducing-perq</id><content type="html" xml:base="https://tucker.wales/writing/introducing-perq/"><![CDATA[<p>I spend most of my working day in a terminal, and pull requests are the thing that keeps yanking me out of it. Someone asks for a review, CI goes red, a comment lands - and each time the answer is the same little ritual: alt-tab to the browser, find the tab, wait for GitHub to paint, squint at a diff, click into the Actions logs, scroll. By the time I’m back in my editor I’ve lost the thread of whatever I was doing.</p>

<p><a href="https://github.com/tuckerwales/perq">perq</a> is my attempt to stop doing that. It’s a terminal dashboard for the PRs you care about, built with <a href="https://textual.textualize.io/">Textual</a>, with <a href="https://claude.com/claude-code">Claude Code</a> folded in to do the reading-and-thinking parts - summarise a PR, review it, answer a question, or work out why a check is failing - streamed straight into the TUI.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run perq
</code></pre></div></div>

<h2 id="the-pitch">The pitch</h2>

<p>You open perq and you see three lists: the PRs you’ve opened, the ones waiting on your review, and the ones you’re otherwise tangled up in (assigned, mentioned, commented on). Each row tells you what you actually want to know at a glance - CI state, review decision, the diff size, how many comments, when it last moved.</p>

<p>Arrow-key to a PR, hit <code class="language-plaintext highlighter-rouge">enter</code>, and you’re in the detail view: five tabs for the overview, the conversation, code-review threads, individual CI checks, and the full diff. No page loads, no spinner, no thirty open browser tabs.</p>

<p>The dashboard refreshes itself every sixty seconds - silently, keeping your cursor exactly where it was - and tells you when something meaningful changes. Your CI just went green. Someone approved you. A new review got requested. You don’t go looking for those; they come to you.</p>

<h2 id="why-a-tui">Why a TUI</h2>

<p>Because the terminal is already where I am, and a PR dashboard that lives in a browser tab has the same problem a cheat sheet in a browser tab has - the moment you have to leave what you’re doing to go look at it, you’ve paid the cost the tool was supposed to save you.</p>

<p>A TUI is always one keystroke away, renders instantly, and works the same over SSH on a bad train connection as it does at my desk. It doesn’t fight for focus and it doesn’t ask me to find the right tab. It’s just there when I press the key, and gone when I’m done.</p>

<h2 id="claude-does-the-reading-for-you">Claude does the reading for you</h2>

<p>The part I use most isn’t the dashboard - it’s the four things Claude Code can do once you’re looking at a PR:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">s</code> summarise</strong> - what this PR does, the key changes grouped by area, how risky it is, and anything worth asking the author.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">R</code> review</strong> - an advisory code review with a verdict, findings tied to <code class="language-plaintext highlighter-rouge">file:line</code> with severities, and design and security notes.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">?</code> ask</strong> - type any question about the PR and get an answer grounded in the diff and the conversation.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">d</code> diagnose</strong> - on a failed GitHub Actions check, perq pulls the job logs and Claude tells you what failed, the root cause, a suggested fix, and whether it smells flaky or real.</li>
</ul>

<p>All of this streams into a modal as it’s generated, so you’re reading the first sentence while the rest is still arriving. Crucially, it’s <strong>local and advisory</strong> - none of it is ever posted to GitHub. It’s the equivalent of a colleague leaning over and saying “this looks fine, but check the migration” - a private read, not a public comment. Press <code class="language-plaintext highlighter-rouge">c</code> to copy the output, <code class="language-plaintext highlighter-rouge">escape</code> to dismiss it.</p>

<p>When you <em>do</em> want to act, perq can write to GitHub too - <code class="language-plaintext highlighter-rouge">a</code> to approve, <code class="language-plaintext highlighter-rouge">x</code> to request changes, <code class="language-plaintext highlighter-rouge">c</code> to leave a comment, <code class="language-plaintext highlighter-rouge">C</code> to close. The line between “Claude helps me think” and “I act on GitHub” is deliberate and never blurry.</p>

<h2 id="technically">Technically</h2>

<p>The whole thing hangs off a single GraphQL query. One round-trip to GitHub fills all three dashboard sections, and the detail view is a second query plus the raw diff. perq authenticates by shelling out to <code class="language-plaintext highlighter-rouge">gh auth token</code>, so if you’ve run <code class="language-plaintext highlighter-rouge">gh auth login</code> it just works (it falls back to <code class="language-plaintext highlighter-rouge">$GITHUB_TOKEN</code>).</p>

<p>The notifications are the bit I’m quietly pleased with. Every refresh is diffed against the previous snapshot, and only <em>transitions</em> raise a toast - CI failing or recovering, a review decision changing, comment counts going up, a new review request appearing. Absolute states never fire, so startup is silent and a PR simply showing up on your dashboard doesn’t nag you. If more than five things changed at once, they collapse into a single “dashboard updated” line instead of a wall of toasts. On macOS, when the terminal isn’t focused, the same events become desktop banners via <code class="language-plaintext highlighter-rouge">osascript</code>.</p>

<p>The Claude integration is a thin wrapper around the CLI. perq spawns <code class="language-plaintext highlighter-rouge">claude -p --output-format stream-json --max-turns 1</code>, feeds it a self-contained prompt, and parses the streaming JSON for text deltas as they come. The prompts ship <em>everything</em> Claude needs inline - the PR metadata, description, conversation, review threads, and the diff (truncated if it’s enormous) - and explicitly tell it not to use any tools. For diagnosis, perq fetches the Actions job logs, strips GitHub’s per-line ISO timestamps, and tails them to fit. It’s one turn, no agent loop, no tool calls: a fast, bounded, read-only ask. The streaming subprocess runs in its own process group so a cancel actually kills it and its children.</p>

<p>There’s a command palette too (<code class="language-plaintext highlighter-rouge">ctrl+p</code>): paste any GitHub PR URL or type a <code class="language-plaintext highlighter-rouge">owner/repo#123</code> shorthand to jump to a PR that isn’t even on your dashboard, or fuzzy-search the ones that are.</p>

<p>It’s about 1,800 lines of Python - Textual for the UI, <code class="language-plaintext highlighter-rouge">httpx</code> for GitHub, and the <code class="language-plaintext highlighter-rouge">gh</code> and <code class="language-plaintext highlighter-rouge">claude</code> CLIs for auth and intelligence. Python 3.12+, managed with <a href="https://docs.astral.sh/uv/">uv</a>.</p>

<h2 id="what-it-isnt">What it isn’t</h2>

<p>perq isn’t trying to be the GitHub web UI in a terminal. There’s no merging, no branch management, no project boards, no notifications inbox. It’s the narrow slice of the PR experience I touch dozens of times a day - <em>what’s waiting on me, what changed, and what does this diff actually do</em> - made fast and keyboard-native, with an LLM doing the tedious reading.</p>

<p>If you want the full surface area of GitHub, it’s a browser tab away. If you want your pull requests to stop pulling you out of the terminal, this is for you.</p>

<h2 id="try-it">Try it</h2>

<p>It’s on <a href="https://github.com/tuckerwales/perq">github.com/tuckerwales/perq</a>. You’ll need Python 3.12+, <a href="https://docs.astral.sh/uv/">uv</a>, the <a href="https://cli.github.com/"><code class="language-plaintext highlighter-rouge">gh</code></a> CLI logged in, and the <code class="language-plaintext highlighter-rouge">claude</code> CLI on your PATH for the summaries and reviews.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run perq
</code></pre></div></div>

<p>Then press <code class="language-plaintext highlighter-rouge">s</code> on your gnarliest open PR and see what Claude makes of it.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[A Textual TUI dashboard for your GitHub pull requests, with Claude Code wired in to summarise, review, and diagnose failing checks - streamed live, without leaving the keyboard.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Joining Engine by Starling</title><link href="https://tucker.wales/writing/joining-engine/" rel="alternate" type="text/html" title="Joining Engine by Starling" /><published>2026-05-28T00:00:00+00:00</published><updated>2026-05-28T00:00:00+00:00</updated><id>https://tucker.wales/writing/joining-engine</id><content type="html" xml:base="https://tucker.wales/writing/joining-engine/"><![CDATA[<p>Last week I <a href="/writing/leaving-monzo/">left Monzo</a>. Today I’m starting at <a href="https://www.enginebystarling.com/">Engine by Starling</a> as a Senior Engineer.</p>

<p><img src="/static/media/joining-engine.gif" alt="I've joined the team at Engine by Starling" /></p>

<p>Engine is the platform that powers Starling Bank, now offered to other banks building on top of it. After five years on banking infrastructure at Monzo, it’s a natural next step - and a chance to work on the platform side of banking from a different angle.</p>

<p>More to share once I’ve found my feet.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[I've joined Engine by Starling as a Senior Engineer.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Leaving Monzo</title><link href="https://tucker.wales/writing/leaving-monzo/" rel="alternate" type="text/html" title="Leaving Monzo" /><published>2026-05-20T00:00:00+00:00</published><updated>2026-05-20T00:00:00+00:00</updated><id>https://tucker.wales/writing/leaving-monzo</id><content type="html" xml:base="https://tucker.wales/writing/leaving-monzo/"><![CDATA[<p>Today is my last day at Monzo.</p>

<p><img src="/static/media/leaving-monzo.jpg" alt="Me on my last day at Monzo, holding an oversized Monzo card" /></p>

<p>I joined as a Backend Engineer in July 2021 and have spent the last (almost) five years in the Platform Collective - the team that builds the things the rest of the bank is built on. It has been, by some distance, the best job I’ve had.</p>

<p>Most of that time was on the bank’s data infrastructure: the platforms and pipelines that move data between our services, AWS, and GCP, and the libraries and tooling that sit on top of them - data access, metadata, annotations, retention, right-to-erasure, the graph database that ties it all together. I picked up a healthy amount of incident response along the way too, which is its own kind of education.</p>

<p>I’m not going to try to summarise five years in a blog post. The short version: I got to work on systems that millions of people use every day, alongside some of the sharpest engineers I’ve ever met, in a place that took both the engineering and the customers seriously. That combination is rarer than it should be.</p>

<p>To everyone I worked with - thank you. You made it.</p>

<h2 id="whats-next">What’s next</h2>

<p>I’ll have more to share soon, but I’m very excited about the future.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[After nearly five years as a Backend Engineer at Monzo, today is my last day.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Introducing Cheetos: Cheat Sheets in Your Menu Bar</title><link href="https://tucker.wales/writing/introducing-cheetos/" rel="alternate" type="text/html" title="Introducing Cheetos: Cheat Sheets in Your Menu Bar" /><published>2026-05-13T00:00:00+00:00</published><updated>2026-05-13T00:00:00+00:00</updated><id>https://tucker.wales/writing/introducing-cheetos</id><content type="html" xml:base="https://tucker.wales/writing/introducing-cheetos/"><![CDATA[<p>I have used <code class="language-plaintext highlighter-rouge">git</code> every working day for the better part of a decade and I still cannot reliably remember the incantation for “undo the last commit but keep my changes staged.” I have a tab open in my browser for it. I have it written on a sticky note that has fallen behind my desk. I have, on more than one occasion, just guessed.</p>

<p>The honest answer is that I don’t need to memorise it. I need it on screen in under a second, the moment I think the question. That is what <a href="https://github.com/tuckerwales/cheetos">Cheetos</a> is for.</p>

<p>It’s a small macOS menu bar app that holds your cheat sheets - vim, tmux, git, docker, kubectl, ssh, curl, find/grep, bash, brew out of the box - and surfaces them with a global hotkey from anywhere. Click a command, it’s on your clipboard. Hit <code class="language-plaintext highlighter-rouge">esc</code>, the window’s gone.</p>

<h2 id="the-pitch">The pitch</h2>

<p>Press your hotkey (or click the menu bar icon). A floating panel appears with a search box. Type a few characters - <code class="language-plaintext highlighter-rouge">commit amend</code>, <code class="language-plaintext highlighter-rouge">tmux split</code>, <code class="language-plaintext highlighter-rouge">kubectl logs</code> - and the matches highlight across every sheet. The top match shows up in a banner; press <code class="language-plaintext highlighter-rouge">↩</code> and the command is copied and the panel disappears. Total interaction time: about a second.</p>

<p>If you’d rather browse, the sidebar lists every sheet. Pick one, scroll, click any backticked command to copy it. <code class="language-plaintext highlighter-rouge">⌘1</code>–<code class="language-plaintext highlighter-rouge">⌘9</code> jumps you between sheets. <code class="language-plaintext highlighter-rouge">⌘F</code> focuses the search box. <code class="language-plaintext highlighter-rouge">⌘,</code> opens settings inline.</p>

<p>That’s the whole app.</p>

<h2 id="why-a-native-menu-bar-app">Why a native menu bar app</h2>

<p>A cheat sheet that lives in a browser tab is not a cheat sheet, it’s a search result. The whole value is being one keystroke away while you’re already in the terminal thinking the question. The moment you have to alt-tab to a browser, find the right tab, and <code class="language-plaintext highlighter-rouge">cmd-F</code> for “amend,” you’ve lost.</p>

<p>Menu bar apps are the right shape for this. They’re always running, always one shortcut away, and they get out of the way the instant you stop looking at them. The window is an <code class="language-plaintext highlighter-rouge">NSPanel</code> - floating, non-activating - so it appears over whatever you’re doing without stealing focus, and hides as soon as you click elsewhere.</p>

<p>The dock icon is suppressed (<code class="language-plaintext highlighter-rouge">LSUIElement = true</code>), so Cheetos doesn’t clutter your <code class="language-plaintext highlighter-rouge">⌘-tab</code> switcher. It’s there, and then it isn’t.</p>

<h2 id="your-sheets-not-mine">Your sheets, not mine</h2>

<p>The bundled sheets are a starting point. Everyone’s muscle memory is different - your tmux is not my tmux, and neither of us cares about half the flags the other one has memorised.</p>

<p>Drop any <code class="language-plaintext highlighter-rouge">.md</code> file into <code class="language-plaintext highlighter-rouge">~/.cheetos/</code> and it shows up in the sidebar. The filename becomes the id, the title is derived from it (<code class="language-plaintext highlighter-rouge">docker-compose.md</code> → “Docker Compose”). Edit the file, save it, and Cheetos picks up the change immediately - it watches the directory with <code class="language-plaintext highlighter-rouge">DispatchSource</code> and rebuilds the library when anything inside changes. No reload, no settings panel, no restart.</p>

<p>You can also hide bundled sheets you don’t use. Right-click in the sidebar, click the eye-slash, done. The sheet stops appearing in search, the sidebar, and the quick-action banner until you bring it back.</p>

<p>The sidebar reorders itself by usage too. Sheets you reach for often float to the top; the ones you forgot you had drift down. It’s the kind of small thing you stop noticing, which is the point.</p>

<h2 id="the-markdown-subset">The markdown subset</h2>

<p>Cheetos renders a deliberately tiny subset of markdown - the parts that matter for cheat sheets:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">#</code>, <code class="language-plaintext highlighter-rouge">##</code>, <code class="language-plaintext highlighter-rouge">###</code> headings to structure a sheet</li>
  <li><code class="language-plaintext highlighter-rouge">-</code> bullets, where lines like <code class="language-plaintext highlighter-rouge">- `cmd` — description</code> render as a copyable command pill next to the description</li>
  <li>Fenced code blocks with a copy button on each one</li>
  <li><code class="language-plaintext highlighter-rouge">`inline code`</code> becomes a clickable pill</li>
  <li><code class="language-plaintext highlighter-rouge">&gt;</code> block quotes for the occasional warning</li>
  <li>Search highlights match in titles, descriptions, and commands</li>
</ul>

<p>That’s it. There’s no images, no tables, no link rendering, no theme switcher. It’s the smallest markdown that does the job, which keeps the renderer a few hundred lines of SwiftUI rather than a dependency.</p>

<p>If you want richer notes, you have a text editor. Cheetos is for the surface area you reach for in the middle of typing a command.</p>

<h2 id="a-few-small-things-that-took-a-while">A few small things that took a while</h2>

<p>A lot of the work in something like this isn’t the obvious stuff - the menu bar plumbing, the markdown parser, the search. It’s the small interactions that decide whether you actually use it.</p>

<p>The quick-action banner is one of those. When you type a search, the top match doesn’t just highlight, it gets pulled out into a banner at the top of the panel that says “press ↩ to copy <code class="language-plaintext highlighter-rouge">git commit --amend</code>.” If you don’t even bother reading the rest of the results, the right answer is one keystroke away. Most of the time it is exactly what you wanted.</p>

<p>The global hotkey is another. It uses Carbon’s <code class="language-plaintext highlighter-rouge">RegisterEventHotKey</code> because that’s still the right way to register a system-wide shortcut on macOS, even in 2026. The toggle behaviour - hotkey opens it, hotkey closes it, <code class="language-plaintext highlighter-rouge">esc</code> closes it, click-away closes it - turns out to require thinking about every combination of “is the panel open, is it focused, is something else active” that you might be in.</p>

<p>And the bit where clicking on a backticked command in the rendered markdown copies it. That’s a <code class="language-plaintext highlighter-rouge">Button</code> inside an <code class="language-plaintext highlighter-rouge">AttributedString</code> inside a <code class="language-plaintext highlighter-rouge">Text</code>, and getting it to look like a pill in the body of a paragraph - without the layout breaking, without focus rings appearing in odd places, without dragging on the cursor change - is the kind of thing that takes an evening for one feature.</p>

<p>None of these are exciting in isolation. Together they’re the difference between an app you open twice and one you open every day.</p>

<h2 id="what-it-isnt">What it isn’t</h2>

<p>Cheetos is not a documentation browser. It’s not a snippet manager. It’s not trying to replace <code class="language-plaintext highlighter-rouge">man</code> or <code class="language-plaintext highlighter-rouge">tldr</code> or your notes app. It is one screen with one job: hand you a short command you almost remember, fast.</p>

<p>If you want a syncing, tagging, multi-device, AI-summarised knowledge base, this isn’t that. If you want your cheat sheets one keystroke away on your own machine, in plain markdown files you control, with no accounts and no telemetry, this is for you.</p>

<h2 id="try-it">Try it</h2>

<p>Grab the latest build from <a href="https://github.com/tuckerwales/cheetos/releases">github.com/tuckerwales/cheetos/releases</a>, or check out the <a href="https://github.com/tuckerwales/cheetos">source</a> - <code class="language-plaintext highlighter-rouge">./build-app.sh</code> produces a <code class="language-plaintext highlighter-rouge">Cheetos.app</code>. macOS 14+.</p>

<p>Drop your own sheets into <code class="language-plaintext highlighter-rouge">~/.cheetos/</code>. Pick a hotkey. Stop guessing at <code class="language-plaintext highlighter-rouge">git reset</code> flags.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[A tiny native macOS app that puts cheat sheets for vim, tmux, git, and the rest of your command-line muscle memory one keystroke away.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Introducing TorDrop: Share Files Over Tor</title><link href="https://tucker.wales/writing/introducing-tordrop/" rel="alternate" type="text/html" title="Introducing TorDrop: Share Files Over Tor" /><published>2026-04-24T00:00:00+00:00</published><updated>2026-04-24T00:00:00+00:00</updated><id>https://tucker.wales/writing/introducing-tordrop</id><content type="html" xml:base="https://tucker.wales/writing/introducing-tordrop/"><![CDATA[<p>Every few months I want to send a file to someone and realise none of the options are quite right. Email has size limits. Cloud drives want accounts on both ends. Most “send a link” services promise privacy on a page full of tracking pixels. AirDrop is great until the recipient is on a different operating system or a different continent.</p>

<p>So I built <a href="https://github.com/tuckerwales/tordrop">TorDrop</a>. It’s a small native macOS app that turns any file on your disk into a <code class="language-plaintext highlighter-rouge">.onion</code> URL you can hand to someone. They open it in Tor Browser and download. You click Stop Sharing. The onion service, and its private key, cease to exist.</p>

<p>No accounts. No servers I run. No persistent keys anywhere.</p>

<h2 id="the-pitch">The pitch</h2>

<p>Open TorDrop, drop one or more files into the window (or use the file picker), and you get a <code class="language-plaintext highlighter-rouge">.onion</code> URL and a QR code. Send the URL to whoever needs the files, over whatever secure channel you already use to talk to them. They open it in Tor Browser and download. When you’re done, you click Stop Sharing and the onion service vanishes.</p>

<p>That’s the whole app.</p>

<p>It’s deliberately minimal. There’s no sign-in, no “my files” list, no history. Nothing is ever uploaded anywhere - the file stays on your disk the entire time, served directly from your machine through Tor.</p>

<p>While a share is starting or live, TorDrop also drops a small status icon into the menu bar. Click it to bring the window back, or drop a file straight onto it to start another share. When nothing is being shared, the menu bar icon disappears.</p>

<h2 id="how-it-works">How it works</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>File → local HTTP server (127.0.0.1:random) → tor → v3 onion service → Recipient
</code></pre></div></div>

<p>When you share a file, TorDrop starts a tiny HTTP server bound to a random loopback port on your machine. It then spawns <code class="language-plaintext highlighter-rouge">tor</code>, connects to its control port, and asks it to create an ephemeral v3 hidden service that forwards onion port 80 to the local HTTP port.</p>

<p>The magic bit is the <code class="language-plaintext highlighter-rouge">ADD_ONION NEW:ED25519-V3 Flags=DiscardPK</code> command. That tells tor to generate a fresh onion service keypair and <em>not</em> hand us the private key. The service exists only inside that running <code class="language-plaintext highlighter-rouge">tor</code> process. When you stop sharing, the process forgets it, and the <code class="language-plaintext highlighter-rouge">.onion</code> address is gone forever. There is nothing on disk to leak, back up, or forget to wipe.</p>

<h2 id="why-native">Why native</h2>

<p>I wanted sharing a file to feel like AirDrop, not like opening a webapp. A regular Mac app you can drag files into, that doesn’t need a browser tab of its own, and that gets out of the way when you’re not using it.</p>

<p>The whole thing is a SwiftUI window backed by an <code class="language-plaintext highlighter-rouge">NSWindowController</code>, with an <code class="language-plaintext highlighter-rouge">NSStatusItem</code> that only appears while a share is active - so you can dismiss the window and still see at a glance that something is being shared, and drop more files on it if you want.</p>

<h2 id="the-surprising-part-writing-an-http-server-by-hand">The surprising part: writing an HTTP server by hand</h2>

<p>TorDrop has exactly one external dependency: the <code class="language-plaintext highlighter-rouge">tor</code> daemon itself. Everything else - the HTTP server, the Tor control-protocol client, the QR code renderer - is first-party code built on the macOS system frameworks.</p>

<p>This wasn’t philosophical purity. It’s that for something whose whole value proposition is “this is doing what it says on the tin,” auditability matters. A dependency tree a mile long undermines the pitch. If you can read the source in an afternoon, you can actually trust it.</p>

<p>So I wrote the HTTP server. It’s a small <code class="language-plaintext highlighter-rouge">Network.framework</code> listener that speaks just enough HTTP/1.1 to serve a directory listing and range-request downloads. I wrote the Tor control-protocol client too - it’s a line-oriented text protocol that’s genuinely pleasant to implement. Between the two, it’s a few hundred lines of Swift. Small enough to reason about, big enough that it definitely works.</p>

<p>There’s something grounding about writing the primitives yourself once in a while. You remember that HTTP is not magic, that a file server is just sockets and strings, that the stack you depend on every day is made of things a person wrote.</p>

<h2 id="security-notes">Security notes</h2>

<p>TorDrop doesn’t try to add anything on top of what Tor itself provides. What it does try to do is not weaken it:</p>

<ul>
  <li><strong>Ephemeral onion keys.</strong> <code class="language-plaintext highlighter-rouge">DiscardPK</code> means the private key never touches disk, never touches Swift memory that might be swapped, and cannot be recovered after the process exits.</li>
  <li><strong>Random URL slug.</strong> Files are served under a <code class="language-plaintext highlighter-rouge">/&lt;20 random chars&gt;/...</code> path, so leaking the onion address alone doesn’t immediately hand over the files. It’s defense in depth, not a secret - you should still treat the full URL as sensitive.</li>
  <li><strong>Loopback-only HTTP.</strong> The embedded server binds to <code class="language-plaintext highlighter-rouge">127.0.0.1</code>. It is not reachable on your LAN, let alone the open internet. The only way in is through the onion service.</li>
  <li><strong>No analytics, no telemetry, no phoning home.</strong> There is nowhere for TorDrop to phone to.</li>
</ul>

<p>The usual Tor caveats still apply. If you need to be certain the URL reached the right person, confirm it through a channel you already trust.</p>

<h2 id="what-its-for-and-what-it-isnt">What it’s for (and what it isn’t)</h2>

<p>TorDrop is for one-off, person-to-person file handoffs where you care about not routing the file through somebody else’s infrastructure. Sending a keyfile to a colleague. Sharing a video with a family member abroad. Getting a big artifact off your machine and onto theirs without it being cached, indexed, or scanned along the way.</p>

<p>It is not a backup tool, not a sync client, not a hosting service. Your machine has to be on for the transfer to work. The moment you stop sharing or quit the app, it’s over.</p>

<h2 id="play-with-it">Play with it</h2>

<p><a href="https://github.com/tuckerwales/tordrop">github.com/tuckerwales/tordrop</a> - source, build instructions, and a Makefile that produces a <code class="language-plaintext highlighter-rouge">.app</code>. You’ll need <code class="language-plaintext highlighter-rouge">brew install tor</code> and macOS 13 or later.</p>

<p>If you try it, I’d love to hear how it went.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[A small native macOS app that hands you a .onion URL for any file. No accounts, no third-party servers, no persistent keys - the onion service dies with the process.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Introducing Tiny Planet: A Browser Game About Getting Unstuck</title><link href="https://tucker.wales/writing/introducing-tiny-planet/" rel="alternate" type="text/html" title="Introducing Tiny Planet: A Browser Game About Getting Unstuck" /><published>2026-04-23T00:00:00+00:00</published><updated>2026-04-23T00:00:00+00:00</updated><id>https://tucker.wales/writing/introducing-tiny-planet</id><content type="html" xml:base="https://tucker.wales/writing/introducing-tiny-planet/"><![CDATA[<p>I’ve been wanting to build a game for a long time. Not a big one - just something small and self-contained that I could actually finish. So when I had a few free weekends, I sat down with Three.js and built <a href="https://tinyplanet.tucker.wales/">Tiny Planet</a>.</p>

<p>It’s a short browser game. You’re an astronaut who’s made a bad landing on an uncharted moon. Your communicator is in pieces, scattered across a tiny procedural world, and you need to repair it and call for rescue.</p>

<p>No install, no sign-up. Open the page, hit start, play.</p>

<h2 id="the-pitch">The pitch</h2>

<p>You wake up next to a crashed landing pod. The planet is small - genuinely small, you can walk around it in a minute or two - so there’s no map, no fast travel, nothing to get lost in. You just explore.</p>

<p>As you wander, you find things: an ancient altar with empty sockets that want to be filled, a still pond with something glinting under the surface, a cave mouth you can’t walk into without waking whatever’s sleeping in there. Each of these is hiding a piece of your broken communicator. Put the pieces back together and you can signal the rescue shuttle.</p>

<p>That’s the whole game. It takes about fifteen minutes to finish.</p>

<h2 id="why-tiny">Why tiny</h2>

<p>The “tiny” isn’t just aesthetic - it’s a design constraint that made the whole project tractable. A small world means a small scope. I didn’t need quest systems, map UI, save files, or a HUD full of icons. The player can see most of the world from anywhere on the surface, so I could lean on sightlines for storytelling rather than markers or objective text.</p>

<p>It also meant I could afford to be generous with detail: bioluminescent plants, drifting fireflies, footprints that fade behind you, a rescue ship with a working tractor beam. The budget I’d have spent on a large world went into small-world atmosphere instead.</p>

<h2 id="technically">Technically</h2>

<p>It’s pure Three.js on top of Vite, no game engine. The planet is a subdivided icosahedron with a noise function driving surface height. Props are placed by raycasting onto the mesh, so trees and rocks sit properly on the curvature. Collision is a cylindrical collider registry that ejects the player horizontally when they walk into something - simple but enough for a game where you don’t jump on things.</p>

<p>Systems communicate through a tiny pub-sub I wrote in about twenty lines. When you pick something up, a <code class="language-plaintext highlighter-rouge">pickup.collected</code> event fires, the inventory reacts, the story system reacts, and whatever spawned the pickup cleans itself up. Nothing knows about anything else it doesn’t need to. That decoupling was the single most useful decision in the codebase - it meant each puzzle could be written as a self-contained module that subscribed to a handful of events.</p>

<p>The atmosphere is a Fresnel shader on a slightly-larger sphere around the planet. The fog blends the horizon into space. The starfield is a single <code class="language-plaintext highlighter-rouge">Points</code> mesh with a custom shader for twinkle. Everything renders in one scene.</p>

<h2 id="controls">Controls</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>W / S        run forward / backward
A / D        turn left / right
Shift        walk (hold while moving)
Space        jump
E            interact / pick up
F            throw held item
I            toggle inventory
Scroll       zoom camera
</code></pre></div></div>

<p>No mouse required. This was deliberate - I wanted it to feel like an arcade game you could play on a laptop trackpad on a train.</p>

<h2 id="a-reminder">A reminder</h2>

<p>The thing I didn’t expect when I started this was how much fun I had.</p>

<p>When you write software for a living, it’s easy to forget that the activity itself is enjoyable. Most of the day-to-day is shaped by things that aren’t the code: deadlines, stakeholders, reviewers, the weight of a system that thousands of people depend on. You start to measure your work by whether it shipped, not by whether you liked making it. I think that’s a fair trade for the work I do - the stakes are part of why it matters - but it means the pleasure of just <em>building a thing</em> gets a bit squeezed out.</p>

<p>Tiny Planet had none of that. Nobody was waiting for it. There was no ticket, no standup, no design review, no postmortem hanging over a decision. If I wanted the fireflies to glow a different colour, I just did it. If I wanted to spend an evening making footprints fade behind the player, that was allowed. I’d forgotten how good that feels.</p>

<p>So this is partly a release post and partly a nudge - if you write software for work, and you haven’t built anything silly lately, go build something silly. Not a side project with a business plan. Not a startup idea. Just a thing. You might enjoy yourself more than you remember.</p>

<h2 id="play-it">Play it</h2>

<p><a href="https://tinyplanet.tucker.wales/">tinyplanet.tucker.wales</a> - runs in any modern browser, keyboard only, no install. It takes about fifteen minutes. Headphones recommended.</p>

<p>If you finish it, I’d love to hear what you thought.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[A small Three.js game where a stranded astronaut repairs a broken communicator on an uncharted moon. Runs in your browser, no install.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Introducing Terraviz: See Your Terraform Infrastructure at a Glance</title><link href="https://tucker.wales/writing/introducing-terraviz/" rel="alternate" type="text/html" title="Introducing Terraviz: See Your Terraform Infrastructure at a Glance" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://tucker.wales/writing/introducing-terraviz</id><content type="html" xml:base="https://tucker.wales/writing/introducing-terraviz/"><![CDATA[<p>Infrastructure-as-code is one of the best things to happen to cloud engineering. But let’s be honest - staring at hundreds of lines of HCL and trying to mentally map out how everything connects is not exactly a great time.</p>

<p>That’s why I built Terraviz: <a href="https://www.terraviz.dev/">https://terraviz.dev/</a>.</p>

<p>Terraviz is a browser-based tool that takes your Terraform files and turns them into an interactive infrastructure diagram. Drop in your .tf files, and within seconds you get a visual map of your entire infrastructure - resources, dependencies, and all.</p>

<p>No sign-up. No backend. Your files never leave your browser.</p>

<h2 id="the-problem">The problem</h2>

<p>If you’ve worked on any non-trivial Terraform project, you’ve probably experienced this: you open up a directory with dozens of .tf files, and you need to understand what’s going on. Maybe you’re onboarding onto a new team. Maybe you’re reviewing a PR that touches networking and compute. Maybe you’re trying to figure out why deleting one resource is going to cascade into something unexpected.</p>

<p>You can read the code line by line, grep for references, or try to hold the dependency graph in your head. But at a certain scale, that just doesn’t work.</p>

<p>I wanted something where I could just see it.</p>

<h2 id="what-terraviz-does">What Terraviz does</h2>

<p>You give it your .tf files (drag and drop a folder, or browse for individual files), and it produces an interactive diagram.</p>

<p>Resources are rendered as color-coded nodes organized by category - compute, network, storage, database, security, DNS, CDN, monitoring, and more. Dependencies are auto-detected by parsing HCL reference expressions, so if your EC2 instance references a security group, that connection shows up as an edge in the graph.</p>

<p>Click any node and its direct dependencies light up while everything else dims. Click an edge to highlight the connection and both endpoints. Expand a node to inspect the raw HCL body. Drag, pan, and zoom to explore large infrastructures.</p>

<h2 id="smart-containment">Smart containment</h2>

<p>One of the features I’m most pleased with is smart containment. VPCs and subnets are rendered as visual containers, and resources are automatically placed inside their parent network based on attribute references - <code class="language-plaintext highlighter-rouge">vpc_id</code>, <code class="language-plaintext highlighter-rouge">subnet_id</code>, <code class="language-plaintext highlighter-rouge">vpc_security_group_ids</code>, and so on.</p>

<p>It even handles transitive relationships. If a resource doesn’t directly reference a VPC but references something that’s already inside one, Terraviz figures that out and places it in the right container. This makes it much easier to see the network topology of your infrastructure at a glance.</p>

<h2 id="filter-search-and-export">Filter, search, and export</h2>

<p>Not every diagram needs to show everything. Terraviz lets you toggle resource categories on and off to focus on specific layers - just networking, just compute, just security. The diagram re-layouts automatically when you hide or show categories.</p>

<p>There’s a search bar (Cmd+F / Ctrl+F) that lets you find any resource and zoom straight to it. Useful when you’re looking at a large project and need to locate something specific.</p>

<p>When you need to share what you’re looking at, you can export the diagram as a high-resolution PNG or SVG - great for documentation, architecture reviews, or presentations.</p>

<h2 id="multi-cloud-support">Multi-cloud support</h2>

<p>The parser works with any Terraform provider, but category detection and containment grouping have enhanced support for AWS, GCP, and Azure. This means VPCs, virtual networks, subnetworks, and their child resources are all grouped correctly regardless of which cloud you’re working with.</p>

<p>Resources from other providers still show up - they’re categorized using general pattern matching and displayed without containment grouping.</p>

<h2 id="privacy-first">Privacy first</h2>

<p>This was a non-negotiable for me. Terraform files often contain sensitive information - resource names that reveal internal architecture, variable defaults, provider configurations. Terraviz runs entirely in your browser. Your .tf files are parsed client-side, never uploaded anywhere, and never stored. There is no backend.</p>

<h2 id="light-and-dark-mode">Light and dark mode</h2>

<p>Because of course there is. Both themes have distinct color palettes for each resource category, and your preference is persisted across sessions.</p>

<h2 id="try-it-out">Try it out</h2>

<p>If you work with Terraform, give Terraviz a try. I’d love to hear what you think - there’s a feedback button right in the app.</p>

<p>Whether you’re onboarding, reviewing, debugging, or just trying to get a clearer picture of your infrastructure, I hope it saves you some time.</p>]]></content><author><name>Joshua Tucker</name></author><summary type="html"><![CDATA[A browser-based tool that turns your Terraform files into interactive infrastructure diagrams. No sign-up, no uploads - everything runs locally.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://tucker.wales/static/media/og-default.jpg" /><media:content medium="image" url="https://tucker.wales/static/media/og-default.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>