<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- generator="FeedCreator 1.8" --><rss version="2.0">
    <channel xmlns:g="http://base.google.com/ns/1.0">
        <title>Andreas Gohr: Linkblog [splitbrain.org]</title>
        <description>Noteworthy or interesting links collected by Andreas Gohr.</description>
        <link/>
        <lastBuildDate>Sun, 06 Sep 2026 19:07:58 +0000</lastBuildDate>
        <generator>FeedCreator 1.8</generator>
        <item>
            <title>Index of Aesthetics</title>
            <link>https://cari.institute/aesthetics</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;div id="filtersFieldset"&gt;
          &lt;legend&gt;Search and filter by...&lt;/legend&gt;

          &lt;p&gt;&lt;label for="keyword"&gt;Keyword&lt;/label&gt;
            
          &lt;/p&gt;

          &lt;div id="startEraFilter"&gt;
  &lt;p&gt;&lt;label for="startEraBound"&gt;Earliest Known Example&lt;/label&gt;&lt;/p&gt;&lt;div&gt;
    
    &lt;p&gt;&lt;label for="startEraSpecifier1"&gt;and&lt;/label&gt;
      
      
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
          
          &lt;div id="endEraFilter"&gt;
  &lt;p&gt;&lt;label for="endEraBound"&gt;End of Popularity&lt;/label&gt;&lt;/p&gt;&lt;div&gt;
    
    &lt;p&gt;&lt;label for="endEraSpecifier1"&gt;and&lt;/label&gt;
      
      
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;
          

          &lt;p&gt;&lt;label for="decade"&gt;
              Relevant Decade
              &lt;abbr title="The decade that an aesthetic was most prevalent in, regardless of the year of its earliest known example or end of popularity. For example, Radical Surrealism originated in the 1960s, but its relevant decade is 1970."&gt;
                &lt;i&gt;&lt;/i&gt;
              &lt;/abbr&gt;
            &lt;/label&gt;
            
          &lt;/p&gt;
        &lt;/div&gt;</description>
            <pubDate>Sat, 22 Aug 2026 21:45:24 +0000</pubDate>
            <guid>https://cari.institute/aesthetics</guid>
        </item>
        <item>
            <title>On the frustration of agreeing with everyone about AI</title>
            <link>https://www.flourish.org/2026/06/agree-everyone-ai/</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;article id="post-agree-everyone-ai" data-pagefind-body&gt;
  

  &lt;div&gt;
    &lt;p&gt;Conversations about AI are febrile and have been for a while now. Everyone has different views.&lt;/p&gt;
&lt;p&gt;It’s frustrating for me because not only do I empathise with all of you, in some ways I
have every major position in my mind.&lt;/p&gt;
&lt;p&gt;I find myself impossible, messed up. Niller-nally, retwixt, fragmented.&lt;/p&gt;
&lt;h3 id="i-agree-with-the-accelerationists"&gt;I agree with the accelerationists&lt;/h3&gt;
&lt;p&gt;I briefly used an open source agentic programming tool with &lt;a href="https://www.cerebras.ai/"&gt;1000 tokens/second&lt;/a&gt;
of a top Chinese model. It was like a drug pumped straight into my veins. I worked on
&lt;a href="https://creatures.wiki/Francis_Irving"&gt;artificial life&lt;/a&gt; 27 years ago - we were trying to
do the same at CyberLife, just much earlier. Creation can be beautiful, and helpful, warm and
purposeful. There’s diseases to cure, climate models to optimise, software to make perfect
for each person.&lt;/p&gt;
&lt;h3 id="i-agree-with-the-artists"&gt;I agree with the artists&lt;/h3&gt;
&lt;p&gt;Our work has all been stolen, and remixed into a bland slop of the world’s cultural
heritage. I’m more radical here than nearly anyone - I think
&lt;a href="https://arxiv.org/abs/2407.13493"&gt;model weights are a derived work&lt;/a&gt;
of every bit of content they’re trained on. Power is winning, not rules. Because power
wins. We can’t keep human emotion in art without a relentless disavowal of AI-generated
art. Every single nuance needs editing, curating. I care about human emotion in art.&lt;/p&gt;
&lt;h3 id="i-agree-with-the-nationalists"&gt;I agree with the nationalists&lt;/h3&gt;
&lt;p&gt;I watched the US administration &lt;a href="https://www.anthropic.com/news/fable-mythos-access"&gt;suddenly declare&lt;/a&gt;
that a powerful new model is available only to US citizens.  It turned my stomach.
My empathy runs high when the European Commission responds that
Europe needs &lt;a href="https://www.reuters.com/legal/litigation/eu-commission-looking-practical-consequences-anthropic-decision-spokesperson-2026-06-14/"&gt;“technological sovereignty”&lt;/a&gt; in AI. It’s a race.&lt;/p&gt;
&lt;h3 id="i-agree-with-the-doomers"&gt;I agree with the doomers&lt;/h3&gt;
&lt;p&gt;I look at &lt;a href="https://pauseai.uk/"&gt;PauseAI&lt;/a&gt; and think… Yep, this technology can
cause harm. &lt;em&gt;Existential&lt;/em&gt; harm - if not with with current architectures as they scale,
then at some point in the next decade or next century with another technology.  It is
necessary to regulate it, to sign international treaties. It’s nukes but accelerated not
only by Government dominance, but also by business value.&lt;/p&gt;
&lt;h3 id="i-agree-with-the-openness-freaks"&gt;I agree with the openness freaks&lt;/h3&gt;
&lt;p&gt;I’ve loved open source software &lt;a href="https://www.flourish.org/webmask/about.html"&gt;since 1997&lt;/a&gt;. So when
I see an argument that &lt;a href="https://rbren.substack.com/p/banning-open-weight-models-would"&gt;open models level the playing field&lt;/a&gt;,
I’ve already watched it play out with software, and despite the problems, I agree. As the models work better and
better, it is worse and worse that we’re locked into a limited set of providers, that
power accumulates with the few.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(I guess I don’t really agree with people opposed to data centres because of water or power
use - as that seems solvable with the right infrastructure. And the other issues dominate
those proximate questions, however much of an environmentalist I am. There are stronger
reasons to oppose data centres.)&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="-breathe-"&gt;… breathe …&lt;/h3&gt;
&lt;p&gt;Yes that all contradicts. It fragments. Open models is the worst for doom, AI sovereignty
is the opposite of a pause, protecting artists’ works blocks acceleration. Nothing
functions.&lt;/p&gt;
&lt;p&gt;Why am I this splinter of contradictions?&lt;/p&gt;
&lt;p&gt;Because all the arguments are right, but which is right the most depends on what happens.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;If we’re at the &lt;strong&gt;top of the S-curve and AI capability stalls&lt;/strong&gt; with a few more 10%
improvements that cost 2x the amount… Then yes let’s roll it out, let’s make it open
for everyone, and regulate the mundane risks, remove all the problems, gain all the
benefits. Can’t stop capitalism, can get it right.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If AI gets &lt;strong&gt;vastly more capable&lt;/strong&gt;, but somehow without its own “will to power”, then I want
to be in a large empire which controls its use for my benefit. The consequences for
defense are mind-collapsing, unthinkable. The military acceleration unbearable. But what
happens if instead you opt out? It’s bad.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If AI not only gets more powerful, but &lt;strong&gt;we lose control&lt;/strong&gt;, these alien minds start acting
at scale and in a way we lose collective and individual control of. It spreads, either an
inert cancer growing through our civilisation eating it up, or a willful Cthulhu monster
carelessly and deliberately breaking everything apart.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And nobody knows what is going to happen.&lt;/p&gt;
&lt;p&gt;I’m kind of tired, agreeing and disagreeing simultaneously with every post on the topic.&lt;/p&gt;
&lt;p&gt;Frozen.&lt;/p&gt;
&lt;p&gt;I guess I’ll &lt;a href="https://pauseai.uk/campaigns"&gt;email my MP&lt;/a&gt; about holding AI developers
liable for severe harm. That seems a good next step.&lt;/p&gt;

  &lt;/div&gt;
&lt;/article&gt;</description>
            <pubDate>Fri, 21 Aug 2026 08:47:03 +0000</pubDate>
            <guid>https://www.flourish.org/2026/06/agree-everyone-ai/</guid>
        </item>
        <item>
            <title>Setting up a remote environment for agentic coding on a VPS · ma.ttias.be</title>
            <link>https://ma.ttias.be/remote-coding-environment-vps/</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;div&gt;&lt;p&gt;For the last few months my laptop has been the bottleneck in my own workflow. Not the CPU, not the RAM – the fact that it’s a &lt;em&gt;laptop&lt;/em&gt;.&lt;/p&gt;&lt;p&gt;I run long AI coding sessions now. Claude Code churns away on a feature for twenty minutes, I go make coffee, and if I close the lid the session dies with it. So for a while I did the obvious dumb thing: I kept the Mac awake with &lt;a href="https://apps.apple.com/us/app/amphetamine/id937984704" target="_blank" rel="noopener noreferrer"&gt;Amphetamine&lt;/a&gt;
, lid open, plugged in, sitting on a desk I then couldn’t walk away from. The machine doing the work had to be the machine in front of me, powered on, present. That’s backwards.&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/amphetamine.jpg" alt="The Amphetamine menu bar app on macOS, its “Start New Session” menu expanded to the Hours submenu, offering to keep the Mac awake anywhere from 1 to 24 hours" width="750" height="777" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;So I moved the whole thing off my laptop. There’s now a server that runs the agents, the code, the databases, and the editor GUI. I reach it from my MacBook at my desk, from the couch, or from my phone on the train. Close the laptop, the work keeps running on the server. Open my phone twenty minutes later and I’m looking at the exact same session, right where it was.&lt;/p&gt;&lt;p&gt;If you’ve used &lt;a href="https://code.claude.com/docs/en/remote-control" target="_blank" rel="noopener noreferrer"&gt;Claude Code’s Remote Control&lt;/a&gt;
to pick a session up from the phone app, it’s a bit like that, except it’s on by default, running all the time, and it’s my own server with none of the limits. Every project I have, not just the one I remembered to start a remote session for.&lt;/p&gt;&lt;p&gt;This post walks through how it’s built and why each piece is there. It’s also written so you can hand the whole thing to an LLM and have it build the same setup for you. More on that at the end, but keep it in mind as you read: everything here is a real command I ran on a real server, not a sketch.&lt;/p&gt;&lt;h2 id="what-you-get"&gt;What you get&lt;a href="#what-you-get" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;A server in the cloud isn’t new, I’ve had those for twenty years. What’s new is that the machine doing the work is no longer tied to the machine I’m sitting at.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;I can close my laptop.&lt;/strong&gt; The agent, the editor, the database, all of it lives on the server. My laptop is a window, not the engine. Shut the lid, get on a train, nothing stops.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;I hand off between devices instantly.&lt;/strong&gt; Start a task on the MacBook, check on it from my phone in the kitchen, back to the desk. Same session, same state, no syncing, no “let me push this real quick so I can pull it elsewhere”.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;It survives a dropped connection.&lt;/strong&gt; My train goes through a tunnel, SSH drops, the work on the server doesn’t care. I reconnect and reattach.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;No more caffeine hacks.&lt;/strong&gt; Amphetamine, Caffeine, &lt;code&gt;caffeinate&lt;/code&gt;, the little &lt;code&gt;while true; do; done&lt;/code&gt; tricks, … all gone. The laptop is allowed to sleep because it isn’t doing anything important.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It all rests on three pieces: a network layer (Tailscale), a server, and a coding GUI you reach from a browser (I use T3, but that part is swappable). Then a bunch of small tweaks on top that make it pleasant instead of fiddly.&lt;/p&gt;&lt;h2 id="tailscale-the-network-that-makes-it-private"&gt;Tailscale: the network that makes it private&lt;a href="#tailscale-the-network-that-makes-it-private" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The first problem with “a server I reach from anywhere” is the “from anywhere” part. You do &lt;em&gt;not&lt;/em&gt; want an editor with a shell on it exposed to the open internet. That’s how you end up in someone’s botnet.&lt;/p&gt;&lt;p&gt;&lt;a href="https://tailscale.com" target="_blank" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt;
solves this cleanly. It’s a mesh VPN built on &lt;a href="https://www.wireguard.com/" target="_blank" rel="noopener noreferrer"&gt;WireGuard&lt;/a&gt;
, but you never touch a WireGuard config. You install a client on each of your devices, log in, and they all end up on one private network (a &lt;em&gt;tailnet&lt;/em&gt;) where they can talk to each other directly, encrypted, no matter which network they’re physically on. My laptop, my phone, and the server all see each other as if they were on the same LAN, whether I’m at home, on cellular, or on hotel wifi.&lt;/p&gt;&lt;p&gt;There are proper native clients for everything: macOS, Windows, Linux, iOS, and Android. On the phone it’s an app-store install and a login, that’s the whole setup. Once a device is on your tailnet it can reach the server by name, and nothing outside the tailnet can.&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/tailscale_screenshot_mac.png" alt="The Tailscale macOS app showing my tailnet: a MacBook, the code-ma-ttias-be server and an iPhone all connected, with the server’s MagicDNS name and Tailscale IPs" width="1600" height="1123" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;That last part is what makes the whole thing safe. The server exposes exactly one port to the public internet: SSH, and even that I keep only as a break-glass door in case Tailscale itself is ever down. Everything else, the editor, the preview servers, the databases, is bound to localhost and published &lt;em&gt;only&lt;/em&gt; onto the tailnet. The firewall denies inbound by default and allows the tailnet interface:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;$ sudo ufw status verbose
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Status: active
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Default: deny &lt;span&gt;(&lt;/span&gt;incoming&lt;span&gt;)&lt;/span&gt;, allow &lt;span&gt;(&lt;/span&gt;outgoing&lt;span&gt;)&lt;/span&gt;, disabled &lt;span&gt;(&lt;/span&gt;routed&lt;span&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;To                         Action      From
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;--                         ------      ----
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;9999/tcp                   ALLOW IN    Anywhere                   &lt;span&gt;# SSH (break-glass, public)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Anywhere on tailscale0     ALLOW IN    Anywhere                   &lt;span&gt;# Tailnet (all services)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tailscale also does the SSH auth for me (&lt;code&gt;tailscale up --ssh&lt;/code&gt;), so device-to-device SSH inside the tailnet needs no key juggling, and it can hand out real, valid HTTPS certificates for your machines. That’s how the editor gets a proper &lt;code&gt;https://&lt;/code&gt; URL with no cert warnings, reachable from Safari on my phone, without ever being public. &lt;code&gt;tailscale serve&lt;/code&gt; puts a localhost port onto the tailnet over HTTPS and nothing more.&lt;/p&gt;&lt;p&gt;If you only copy one thing from this post: put the server on a tailnet and keep it off the public internet.&lt;/p&gt;&lt;h2 id="the-server-i-started-on-a-vps-and-outgrew-it"&gt;The server: I started on a VPS and outgrew it&lt;a href="#the-server-i-started-on-a-vps-and-outgrew-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;For the first month this ran on a DigitalOcean droplet: Ubuntu 26.04 LTS, 2 vCPUs, about 8 GB of RAM, 28 GB of disk, plus a 100 GB block volume I added a few days later. That was the right way to start. It’s cheap, it’s up in sixty seconds, and it proved the idea worked before I spent real money on it.&lt;/p&gt;&lt;p&gt;Then I started actually using it.&lt;/p&gt;&lt;p&gt;Two vCPUs cope fine with one agent. They don’t cope with four. Every T3 thread is its own worktree with its own preview server, so when two of them are running a test suite while a third builds assets, two vCPUs are gone. Here’s &lt;code&gt;sar&lt;/code&gt; on my last morning on that machine, peak one-minute load per day:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;$ &lt;span&gt;for&lt;/span&gt; f in /var/log/sysstat/sa&lt;span&gt;[&lt;/span&gt;0-9&lt;span&gt;]&lt;/span&gt;*&lt;span&gt;;&lt;/span&gt; &lt;span&gt;do&lt;/span&gt; sar -q -f &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; awk -v &lt;span&gt;d&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;basename &lt;span&gt;$f&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;'NR&amp;gt;3 &amp;amp;&amp;amp; $4 ~ /^[0-9.]+$/{if($4&amp;gt;m)m=$4} END{if(m)printf "  %s  %.2f\n", d, m}'&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  sa22   4.26
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  sa23   3.14
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  sa24   4.63
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  sa25  15.04
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;  sa26  19.28
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Load 19 on two cores means everything is queued behind everything else. And 3 GB of the 8 had been pushed out to swap, so the box was reading its own memory back off a disk.&lt;/p&gt;&lt;p&gt;I didn’t move because the VPS got expensive. I moved because &lt;em&gt;fixing&lt;/em&gt; it on the same provider would have been. A CPU-Optimized droplet with 32 vCPUs and 64 GB of RAM is $672 a month. The box I ended up with is €137.&lt;/p&gt;&lt;p&gt;It’s from the &lt;a href="https://www.hetzner.com/sb/" target="_blank" rel="noopener noreferrer"&gt;Hetzner server auction&lt;/a&gt;
: an i9-13900 (24 cores, 32 threads), 64 GB of DDR5 ECC, and two 1.92 TB datacenter NVMe drives, in Falkenstein. €137/month excluding VAT, no setup fee. The auction is second-hand hardware they’re clearing out, so you’re picking from whatever’s in stock that day rather than a menu, and the price ratchets down on a timer until someone buys it.&lt;/p&gt;&lt;p&gt;A word on picking one, because “more cores is better” is only half right. Most of what an agent makes you wait on is single-threaded: &lt;code&gt;phpstan&lt;/code&gt;, &lt;code&gt;composer&lt;/code&gt;, a &lt;code&gt;hugo&lt;/code&gt; build, one &lt;code&gt;pest&lt;/code&gt; process. There was a 32-core EPYC in the same auction for twice the money whose single-thread score is &lt;em&gt;lower&lt;/em&gt; than the droplet I was leaving. It would have felt slower for one agent while being great for twelve. The i9 happened to be both the fastest single core and the most cores in that day’s German stock.&lt;/p&gt;&lt;p&gt;The same benchmark on both boxes:&lt;/p&gt;&lt;table readabilityDataTable="1"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Old (2 vCPU droplet)&lt;/th&gt;&lt;th&gt;New (i9-13900)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;PHP, 3M md5, one thread&lt;/td&gt;&lt;td&gt;0.58s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.36s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;PHP, 3M md5, all cores&lt;/td&gt;&lt;td&gt;0.88s (×2)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.74s (×32)&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;xz -3&lt;/code&gt; on 64 MB, one thread&lt;/td&gt;&lt;td&gt;29.2s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;9.6s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;xz -3&lt;/code&gt; on 64 MB, all cores&lt;/td&gt;&lt;td&gt;17.7s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3.4s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;openssl sha256&lt;/code&gt;&lt;/td&gt;&lt;td&gt;456 MB/s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2,587 MB/s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;openssl aes-256-gcm&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;10,654 MB/s&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;7,717 MB/s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;dd&lt;/code&gt; 1 GB write&lt;/td&gt;&lt;td&gt;966 MB/s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2.5 GB/s&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Note the AES line, because it’s the one thing that got &lt;em&gt;worse&lt;/em&gt;. The old Xeon has AVX-512 and this i9 doesn’t (Intel fused it off on consumer Raptor Lake), so OpenSSL drops from the AVX-512 VAES path to the 256-bit one. The trade runs the other way on SHA-256, where the i9 has SHA-NI and the Xeon didn’t, hence the 5.7x. Nothing I run is bottlenecked on AES throughput, so I’ll take it. But a faster CPU isn’t automatically faster at everything.&lt;/p&gt;&lt;p&gt;On top of the base OS it’s just a normal dev machine: git, tmux, PHP, Node, a database or two, … and Docker for anything that’s easier in a container.&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/agentic_coding_linux_box.png" alt="htop on the original VPS: two CPUs, 2.8 GB of 7.75 GB RAM in use, and a process list showing ClickHouse, T3’s node process, tailscaled, and several parallel claude agents running as the dev user" width="1680" height="980" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;&lt;em&gt;(That screenshot is the old droplet, back when two cores were still coping.)&lt;/em&gt;&lt;/p&gt;&lt;p&gt;I also turn &lt;em&gt;off&lt;/em&gt; automatic updates on this server, which feels wrong until you think about it. Unattended-upgrades can decide to reboot at 3am, and a reboot kills every running tmux session and every in-flight agent. That’s the exact thing this setup exists to prevent. So I patch by hand, when I choose to, and let the sessions live:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;$ sudo apt-get purge unattended-upgrades
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;$ sudo systemctl mask apt-daily.timer apt-daily-upgrade.timer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Long-running work goes in &lt;code&gt;tmux&lt;/code&gt;, one session per project. Detach with &lt;code&gt;Ctrl-b d&lt;/code&gt;, reattach from any device with &lt;code&gt;tmux attach&lt;/code&gt;. That’s the fallback layer that survives everything, including the fancier GUI on top of it dying. If the browser editor ever breaks, &lt;code&gt;ssh&lt;/code&gt; in and &lt;code&gt;tmux attach&lt;/code&gt; and I’ve lost nothing.&lt;/p&gt;&lt;h3 id="one-btrfs-pool-mirrored-across-both-drives"&gt;One btrfs pool, mirrored across both drives&lt;a href="#one-btrfs-pool-mirrored-across-both-drives" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;I got this wrong when I first built this server.&lt;/p&gt;&lt;p&gt;Cloud images hand you ext4 as the disk by default. ext4 can’t share blocks between files, so every worktree with its own &lt;code&gt;node_modules&lt;/code&gt; costs the full copy. Ten of them took that 28 GB disk down to 687 MB free in about a day, and I ended up &lt;a href="/deduplicating-git-worktrees-ext4-jdupes/"&gt;deduplicating the worktrees with jdupes&lt;/a&gt;
to get the space back. Hardlinks work. They’re still a workaround for a filesystem that can’t share blocks.&lt;/p&gt;&lt;p&gt;On the droplet I fixed that by bolting on a 100 GB block volume formatted btrfs and moving &lt;code&gt;/home/dev&lt;/code&gt; onto it. It worked, but it left the box with a 28 GB root that had nothing to do with the 100 GB where the actual work lived, and that root filled up and took the server down twice. Splitting a disk is a capacity-planning problem you get to have forever.&lt;/p&gt;&lt;p&gt;With two NVMe drives I don’t have to split anything. The whole machine is one btrfs pool now, mirrored across both drives, with subvolumes instead of partitions:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;$ cat /proc/mdstat &lt;span&gt;|&lt;/span&gt; grep -E &lt;span&gt;'^md|blocks'&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;md2 : active raid1 nvme1n1p4&lt;span&gt;[&lt;/span&gt;1&lt;span&gt;]&lt;/span&gt; nvme0n1p4&lt;span&gt;[&lt;/span&gt;0&lt;span&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;      &lt;span&gt;1864754496&lt;/span&gt; blocks super 1.2 &lt;span&gt;[&lt;/span&gt;2/2&lt;span&gt;]&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;UU&lt;span&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;md1 : active raid1 nvme1n1p3&lt;span&gt;[&lt;/span&gt;1&lt;span&gt;]&lt;/span&gt; nvme0n1p3&lt;span&gt;[&lt;/span&gt;0&lt;span&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;      &lt;span&gt;8379392&lt;/span&gt; blocks super 1.2 &lt;span&gt;[&lt;/span&gt;2/2&lt;span&gt;]&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;UU&lt;span&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;md0 : active raid1 nvme1n1p2&lt;span&gt;[&lt;/span&gt;1&lt;span&gt;]&lt;/span&gt; nvme0n1p2&lt;span&gt;[&lt;/span&gt;0&lt;span&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;      &lt;span&gt;1046528&lt;/span&gt; blocks super 1.2 &lt;span&gt;[&lt;/span&gt;2/2&lt;span&gt;]&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;UU&lt;span&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;$ findmnt -no SOURCE,FSTYPE,OPTIONS /
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;/dev/md2&lt;span&gt;[&lt;/span&gt;/@&lt;span&gt;]&lt;/span&gt; btrfs rw,noatime,compress&lt;span&gt;=&lt;/span&gt;zstd:3,ssd,discard&lt;span&gt;=&lt;/span&gt;async,space_cache&lt;span&gt;=&lt;/span&gt;v2,subvolid&lt;span&gt;=&lt;/span&gt;256,subvol&lt;span&gt;=&lt;/span&gt;/@
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Be warned though, if you’re doing this on Hetzner: &lt;code&gt;installimage&lt;/code&gt; cannot build a native btrfs RAID1. Its own docs say so, in as many words: “no support btrfs multi-device volumes”. So the mirror is mdadm and btrfs sits on top of it. You still get reflinks, compression, subvolumes and snapshots, which is everything this setup actually uses. What you give up is btrfs repairing a bad block from the good copy, because mdadm can’t tell it which half is right. btrfs will still &lt;em&gt;detect&lt;/em&gt; the corruption. It just can’t fix it for you.&lt;/p&gt;&lt;p&gt;A couple of things that cost me a cycle there. The EFI partition can’t live on mdadm (firmware can’t read it), so there’s a separate 1 GB ESP on each drive. And &lt;code&gt;installimage&lt;/code&gt; mounts your subvolumes &lt;em&gt;before&lt;/em&gt; it unpacks the image, so a mountpoint nested deeper than one level fails with ENOENT: my &lt;code&gt;@var-lib-docker&lt;/code&gt; subvolume blew up the install at step 7 of 17 because &lt;code&gt;/var/lib&lt;/code&gt; didn’t exist yet. &lt;code&gt;@home&lt;/code&gt; and &lt;code&gt;@tmp&lt;/code&gt; were fine. Create the deep ones afterwards.&lt;/p&gt;&lt;p&gt;Copy-on-write is the reason to bother with btrfs at all. &lt;code&gt;cp --reflink&lt;/code&gt; clones a file by sharing its blocks instead of copying them, so duplicating a gigabyte costs nothing until something writes:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;$ sudo btrfs filesystem df / &lt;span&gt;|&lt;/span&gt; grep ^Data
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Data, single: &lt;span&gt;total&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;23.01GiB, &lt;span&gt;used&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;18.39GiB
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;$ cp --reflink&lt;span&gt;=&lt;/span&gt;always -a node_modules /home/dev/scratch/    &lt;span&gt;# 520 MB, 32,985 files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;$ sudo btrfs filesystem df / &lt;span&gt;|&lt;/span&gt; grep ^Data
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Data, single: &lt;span&gt;total&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;23.01GiB, &lt;span&gt;used&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;18.39GiB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That’s ohdear.app’s &lt;code&gt;node_modules&lt;/code&gt;, half a gigabyte across nearly 33,000 files, cloned in 0.42 seconds, and the used figure doesn’t move at all. Ten checkouts of the same &lt;code&gt;node_modules&lt;/code&gt; are that problem at scale, and this is &lt;code&gt;worktree-up&lt;/code&gt;’s whole trick: if the worktree’s lockfile is byte-identical to the primary’s, it reflinks the dependency directory instead of installing. A fresh Laravel worktree with &lt;code&gt;vendor&lt;/code&gt; and &lt;code&gt;node_modules&lt;/code&gt; both cloned is ready in 1.5 seconds instead of a couple of minutes.&lt;/p&gt;&lt;p&gt;I mount it with &lt;code&gt;compress=zstd:3&lt;/code&gt; too, so 11 GB of home sits in 7.4 GB on disk:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;$ sudo compsize /home/dev
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Processed &lt;span&gt;335505&lt;/span&gt; files, &lt;span&gt;139334&lt;/span&gt; regular extents &lt;span&gt;(&lt;/span&gt;&lt;span&gt;140869&lt;/span&gt; refs&lt;span&gt;)&lt;/span&gt;, &lt;span&gt;232539&lt;/span&gt; inline.
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;Type       Perc     Disk Usage   Uncompressed Referenced
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;TOTAL       62%      7.4G          11G          11G
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;none       100%      5.7G         5.7G         5.7G
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;zstd        28%      1.7G         6.1G         6.0G
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The 100% line is everything already compressed (images, archives, git packs), which btrfs correctly declines to compress twice. The zstd line is the text: source, &lt;code&gt;node_modules&lt;/code&gt;, SQL dumps.&lt;/p&gt;&lt;p&gt;The jdupes cron still runs, and on btrfs it now uses &lt;code&gt;jdupes -B&lt;/code&gt;, which dedupes matching files into shared extents rather than hardlinking them, so writing to one copy can’t reach the others.&lt;/p&gt;&lt;h3 id="the-full-disk-layout-tmp-included"&gt;The full disk layout, &lt;code&gt;/tmp&lt;/code&gt; included&lt;a href="#the-full-disk-layout-tmp-included" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;There’s a second default I got wrong, and it ships with every cloud image: &lt;code&gt;/tmp&lt;/code&gt; is a tmpfs, so it lives in RAM. Ubuntu sizes it at half your memory, 3.9 GB here. A tmpfs page can’t be reclaimed until the file is deleted, so every gigabyte of build scratch stays charged to memory until something cleans it up. I caught &lt;code&gt;/tmp&lt;/code&gt; sitting at 1.9 GB used while &lt;code&gt;Shmem&lt;/code&gt; was only 122 MB, which means about 1.75 GB of it had been pushed out into swap. At 81% full the box had eaten its whole memory headroom: &lt;code&gt;fork&lt;/code&gt; started failing with ENOMEM and even &lt;code&gt;echo&lt;/code&gt; returned nothing.&lt;/p&gt;&lt;p&gt;So &lt;code&gt;/tmp&lt;/code&gt; is its own subvolume in the pool now, and the ceiling went from 3.9 GB of memory to the whole 1.8 TB. Be warned though, a tmpfs gets wiped at every boot and a real filesystem doesn’t, so you want a &lt;code&gt;D /tmp 1777 root root 10d&lt;/code&gt; line in &lt;code&gt;/etc/tmpfiles.d/&lt;/code&gt; to clear it at boot and age out whatever survives. &lt;code&gt;nosuid,nodev&lt;/code&gt; because &lt;code&gt;/tmp&lt;/code&gt; is world-writable, and &lt;code&gt;noatime&lt;/code&gt; so reading a file doesn’t cause a write.&lt;/p&gt;&lt;p&gt;The whole fstab, &lt;code&gt;/boot&lt;/code&gt; and swap included, because there’s nothing else to it:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;B358-5484     /boot/efi       vfat  &lt;span&gt;umask&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0077&lt;/span&gt; &lt;span&gt;0&lt;/span&gt; &lt;span&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;1bbb62f6-...  /boot           ext4  defaults &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;c0f199f5-...  none            swap  sw &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;fcfad565-...  /               btrfs defaults,noatime,compress&lt;span&gt;=&lt;/span&gt;zstd:3,subvol&lt;span&gt;=&lt;/span&gt;@ &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;fcfad565-...  /home           btrfs defaults,noatime,compress&lt;span&gt;=&lt;/span&gt;zstd:3,subvol&lt;span&gt;=&lt;/span&gt;@home &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;fcfad565-...  /tmp            btrfs defaults,noatime,nosuid,nodev,compress&lt;span&gt;=&lt;/span&gt;zstd:3,subvol&lt;span&gt;=&lt;/span&gt;@tmp &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;fcfad565-...  /.snapshots     btrfs defaults,noatime,compress&lt;span&gt;=&lt;/span&gt;zstd:3,subvol&lt;span&gt;=&lt;/span&gt;@snapshots &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;UUID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;fcfad565-...  /var/lib/docker btrfs defaults,noatime,compress&lt;span&gt;=&lt;/span&gt;zstd:3,subvol&lt;span&gt;=&lt;/span&gt;@var-lib-docker &lt;span&gt;0&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Five of those lines are the same filesystem. That’s the point of subvolumes: no line has a size, so nothing can fill up while something else sits half empty.&lt;/p&gt;&lt;h2 id="the-coding-gui-t3-but-bring-your-own"&gt;The coding GUI: T3, but bring your own&lt;a href="#the-coding-gui-t3-but-bring-your-own" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This layer is the least settled of the three, and that’s fine.&lt;/p&gt;&lt;p&gt;I want a proper editor I can drive from a browser, including my phone’s browser, so I’m not squinting at &lt;code&gt;tmux&lt;/code&gt; on a 6-inch screen when I just want to review a diff and approve a change. The server runs the editor as a service: I open a URL and there’s my project, my files, my git state, my agents.&lt;/p&gt;&lt;p&gt;Right now I use &lt;a href="https://github.com/pingdotgg/t3code" target="_blank" rel="noopener noreferrer"&gt;T3 Code&lt;/a&gt;
for this. It’s a web GUI that runs &lt;em&gt;on&lt;/em&gt; the server (not on my Mac), speaks to Claude Code and Codex, does git worktrees, shows diffs, and opens pull requests. It has first-class Tailscale support, so &lt;code&gt;t3 serve --tailscale-serve&lt;/code&gt; publishes it on tailnet-only HTTPS and I open that URL from anything. It’s early software, the project says so itself, and I’ve hit rough edges. But it does the job today.&lt;/p&gt;&lt;p&gt;My one current issue with T3 is that I can’t queue up multiple messages in the editor: a new message can only be sent once the agent has finished processing the previous one. That’s a matter of time, I imagine, before that’s fixed.&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/webgui_example.jpg" alt="The T3 Code web GUI in a browser: a projects sidebar with ma.ttias.be, Box admin, Oh Dear and Home Assistant, an agent thread showing a git diff, and the Claude Opus model picker at the bottom" width="1680" height="1045" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update, August 2026:&lt;/strong&gt; there’s a native iOS app now, &lt;a href="https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824" target="_blank" rel="noopener noreferrer"&gt;T3 Code - Remote Claude &amp;amp; more&lt;/a&gt;
, and it’s replaced the browser-on-my-phone half of this for me. It doesn’t replace the server, though. The app pairs with the same T3 backend running on the server, the one serving the web GUI in the screenshot above, so everything below still applies. I just get a proper app icon instead of a Safari tab.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;This piece is swappable, and chances are, I’ll end up trying a few different web GUIs over the coming weeks.&lt;/strong&gt; The whole space of “browser-based agent coding UIs” is moving fast, and a year from now the good option might be something else entirely. Nothing else in this setup depends on the specific GUI. The network is Tailscale, the server is a plain Ubuntu box, the work lives in git and &lt;code&gt;tmux&lt;/code&gt;. If I rip out T3 tomorrow and drop in a different editor, the foundation doesn’t move. Pick whatever’s good &lt;em&gt;now&lt;/em&gt;, and don’t marry it.&lt;/p&gt;&lt;p&gt;T3 itself isn’t the recommendation here, the &lt;em&gt;shape&lt;/em&gt; is. A few others solve the same problem already: &lt;a href="https://emdash.ai/" target="_blank" rel="noopener noreferrer"&gt;Emdash&lt;/a&gt;
and &lt;a href="https://github.com/stablyai/orca" target="_blank" rel="noopener noreferrer"&gt;Orca&lt;/a&gt;
are open-source apps that run agents in parallel git worktrees, with remote-over-SSH execution so the runtime can sit on another machine. &lt;a href="https://github.com/siteboon/claudecodeui" target="_blank" rel="noopener noreferrer"&gt;CloudCLI&lt;/a&gt;
and &lt;a href="https://paseo.sh/" target="_blank" rel="noopener noreferrer"&gt;Paseo&lt;/a&gt;
go the web route like T3: a self-hosted server you reach from a browser or your phone. I haven’t run all of them in anger, so this isn’t a leaderboard.&lt;/p&gt;&lt;h2 id="no-code-editor"&gt;No code editor&lt;a href="#no-code-editor" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I haven’t opened a real code editor in months. No VS Code, no PhpStorm, no vim beyond a quick &lt;code&gt;:wq&lt;/code&gt; on a config file. And I don’t miss it.&lt;/p&gt;&lt;p&gt;That’s the biggest change in how I work. I assumed the browser GUI was me swapping my editor for a worse one I could reach from my phone. Turns out I don’t use it as an editor at all. I open T3 to read a diff, approve a change, start the next task, and watch an agent work. I almost never type code into a file by hand.&lt;/p&gt;&lt;p&gt;The loop now is: describe what I want, let the agent write it, read the diff, run the tests, say what’s wrong, go again. The editor used to be where I did that middle step. It’s mostly gone, and what’s left is the two ends, deciding what to build and judging whether it’s right. Those are the parts I want to be doing anyway.&lt;/p&gt;&lt;p&gt;So what I tuned the setup for isn’t editing, it’s that loop: how fast I get from an idea to a diff I can judge, from anywhere, without a machine I have to keep awake. A snappy editor does nothing for that. A server that runs the suite in a couple of seconds and shows me the result on my phone does.&lt;/p&gt;&lt;p&gt;I’m not claiming hand-coding is dead. I still read every line before it merges, and I still drop into the code when an agent is flailing. But the editor as the place I spend my day is over.&lt;/p&gt;&lt;h2 id="the-custom-sauce"&gt;The custom sauce&lt;a href="#the-custom-sauce" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Everything above is the skeleton. These are the tweaks that make it feel like &lt;em&gt;mine&lt;/em&gt; rather than a generic tutorial, and they’re the bits I’d miss if they were gone.&lt;/p&gt;&lt;h3 id="the-agents-update-themselves-every-morning"&gt;The agents update themselves every morning&lt;a href="#the-agents-update-themselves-every-morning" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Claude Code, Codex, and T3 all ship new versions constantly. I don’t want to &lt;code&gt;npm update&lt;/code&gt; three tools by hand every morning, and I don’t want stale versions either. A systemd timer updates all three at 05:00 my time, before I’m awake, so whatever I open is already current.&lt;/p&gt;&lt;p&gt;It runs on UTC (as servers should), but I pin the schedule to my actual timezone, so daylight saving never shifts it:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="ini"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;[Timer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# Box runs UTC; the explicit timezone keeps this at 05:00 local year-round.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;OnCalendar&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;*-*-* 05:00:00 Europe/Brussels&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;Persistent&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that looks like it clashes with turning &lt;em&gt;off&lt;/em&gt; automatic OS updates earlier, the difference is what can reboot the server. An OS upgrade can, and it drags every session down with it. These don’t touch the OS: Claude and Codex just swap a binary the next run picks up, and T3 only restarts itself. That restart still drops a live T3 connection, which is exactly why it runs at 05:00 and not while I’m working.&lt;/p&gt;&lt;p&gt;T3 gets the careful treatment because it’s alpha software backing the UI I work in: after each update the timer health-checks it, and if the new version doesn’t come back up, it rolls back to the previous one automatically. I tested both paths on the way in, a real upgrade and a forced failure, because an auto-updater you haven’t watched roll back is just a hope.&lt;/p&gt;&lt;h3 id="onboarding-a-workspace-in-one-command"&gt;Onboarding a workspace in one command&lt;a href="#onboarding-a-workspace-in-one-command" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;Adding a project shouldn’t be a checklist I half-remember. &lt;code&gt;newproject&lt;/code&gt; clones the repo, provisions its databases, writes its &lt;code&gt;.env&lt;/code&gt;, and registers it in T3, which can’t add a project from its own UI on a remote server. The whole script is about a dozen lines:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;URL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;:?usage: newproject &amp;lt;git-url&amp;gt; [title]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;NAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;basename &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$URL&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; .git&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;DEST&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/projects/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;git clone &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$URL&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DEST&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;cd&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DEST&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;worktree-init                                    &lt;span&gt;# databases, .env, dependencies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;t3 project add &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DEST&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; --title &lt;span&gt;"&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;:-&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The real work is in &lt;code&gt;worktree-init&lt;/code&gt;. Every project gets its own MySQL and ClickHouse database named &lt;code&gt;proj_&amp;lt;name&amp;gt;&lt;/code&gt;, with the credentials written into &lt;code&gt;.env&lt;/code&gt;. It reaches MySQL through a &lt;code&gt;~/.my.cnf&lt;/code&gt; file instead of &lt;code&gt;sudo mysql&lt;/code&gt;, so the same script runs unattended from cron without hanging on a password prompt:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;NAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;basename &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PWD&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;DBNAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"proj_&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;printf&lt;/span&gt; &lt;span&gt;'%s'&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; tr &lt;span&gt;'A-Z-'&lt;/span&gt; &lt;span&gt;'a-z_'&lt;/span&gt; &lt;span&gt;|&lt;/span&gt; tr -cd &lt;span&gt;'a-z0-9_'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;DBPASS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;sed -n &lt;span&gt;'s/^DB_PASSWORD=//p'&lt;/span&gt; .env &lt;span&gt;|&lt;/span&gt; head -1&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;     &lt;span&gt;# reuse if already set&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt; -n &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DBPASS&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;DBPASS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;openssl rand -hex 16&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;mysql &lt;span&gt;&amp;lt;&amp;lt;SQL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;CREATE DATABASE IF NOT EXISTS \`$DBNAME\` CHARACTER SET utf8mb4;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;CREATE USER IF NOT EXISTS '$DBNAME'@'localhost' IDENTIFIED BY '$DBPASS';
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;ALTER USER '$DBNAME'@'localhost' IDENTIFIED BY '$DBPASS';
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;GRANT ALL PRIVILEGES ON \`$DBNAME\`.* TO '$DBNAME'@'localhost';
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;ALTER USER&lt;/code&gt; right after &lt;code&gt;CREATE USER IF NOT EXISTS&lt;/code&gt; looks redundant, and it’s there because of a bug. &lt;code&gt;IF NOT EXISTS&lt;/code&gt; keeps the &lt;em&gt;old&lt;/em&gt; password when the user already exists, so the first time I deleted a project’s &lt;code&gt;.env&lt;/code&gt; and re-ran, the freshly generated password and the database’s actual password drifted apart and the app couldn’t connect. Setting it every run keeps them in sync.&lt;/p&gt;&lt;p&gt;Serving is the part T3 triggers. When I start a thread, T3 makes a git worktree and runs a single setup script. Mine is &lt;code&gt;worktree-up&lt;/code&gt;: it installs dependencies and puts the app on the tailnet. The fiddly bits are all Laravel:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# APP_URL portless: domain-scoped routes match Request::getHost(), which drops the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# port, so "host:8563" would never match. ASSET_URL WITH the port: Vite builds&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# asset links from it, not the request, so portless sends every stylesheet to :443&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# (that's T3 Code, not the app).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;envset APP_URL   &lt;span&gt;"https://&lt;/span&gt;&lt;span&gt;$HOST&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;envset ASSET_URL &lt;span&gt;"https://&lt;/span&gt;&lt;span&gt;$HOST&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;PHP_CLI_SERVER_WORKERS&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;    &lt;span&gt;# php -S is single-request and self-deadlocks otherwise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;nohup php artisan serve --host&lt;span&gt;=&lt;/span&gt;127.0.0.1 --port&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &amp;gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$LOG&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; 2&amp;gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt;1&lt;/span&gt; &lt;span&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;tailscale serve --bg --https&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;"127.0.0.1:&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every comment in there is a debugging session I only want to have once. The &lt;code&gt;$PORT&lt;/code&gt; comes from &lt;code&gt;worktree-port&lt;/code&gt;, which hashes the worktree name for a starting point and then linear-probes for a free one, so every worktree keeps a stable URL and two of them never fight over the same port. A naive &lt;code&gt;hash % range&lt;/code&gt; collides about one time in five by the time you’ve got a couple of dozen worktrees, and the one that loses just fails to bind without saying anything.&lt;/p&gt;&lt;h3 id="previewing-worktrees"&gt;Previewing worktrees&lt;a href="#previewing-worktrees" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;A reader asked the practical version of this:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;How do you view the output when you’re switching devices? Do you have specific domain names for all your projects so that you can use the browser?&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;No domains. Nothing in DNS, no reverse-proxy vhost, no per-project config anywhere. Every preview is a port on the one hostname the server already has, and that hostname comes from Tailscale: &lt;code&gt;code-ma-ttias-be.tail854658.ts.net&lt;/code&gt;. The app binds to &lt;code&gt;127.0.0.1&lt;/code&gt; on a port nobody outside can reach, and one line publishes it:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;tailscale serve --bg --https&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;"127.0.0.1:&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which gets me this, one entry per running preview:&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;$ tailscale serve status
https://code-ma-ttias-be.tail854658.ts.net:8443 (tailnet only)
|-- / proxy http://127.0.0.1:8088

https://code-ma-ttias-be.tail854658.ts.net:8809 (tailnet only)
|-- / proxy http://127.0.0.1:8809
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Same host every time, different port per worktree. &lt;code&gt;worktree-port&lt;/code&gt; pins that port for the life of the worktree, so a branch’s preview lives at the same address today and next Tuesday.&lt;/p&gt;&lt;p&gt;The certificate is the part that makes this usable on a phone. Tailscale gets a real Let’s Encrypt cert for the MagicDNS name and renews it, so this is ordinary trusted HTTPS, not a self-signed thing I have to click past on iOS:&lt;/p&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;$ echo | openssl s_client -connect code-ma-ttias-be.tail854658.ts.net:8443 \
    -servername code-ma-ttias-be.tail854658.ts.net 2&amp;gt;/dev/null \
    | openssl x509 -noout -issuer -subject
issuer=C=US, O=Let's Encrypt, CN=YE2
subject=CN=code-ma-ttias-be.tail854658.ts.net
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Switching devices is then just… opening the URL. MagicDNS resolves that name identically on my MacBook, my phone, and the iPad, so a link I open at my desk is the same string on the couch. No VPN toggle to flip, no login. And it stays tailnet-only: from the public internet that name points at a &lt;code&gt;100.x&lt;/code&gt; address nobody can route to.&lt;/p&gt;&lt;p&gt;Could I give each project a subdomain instead? Sure, and then every new project needs a DNS record, a wildcard certificate, and a vhost, all for a preview that exists for the three days the branch does. A port costs me nothing and Tailscale already owns the cert.&lt;/p&gt;&lt;p&gt;The catch with ports is that any dev server generating absolute URLs has to be told which one it’s on, or it points every asset at &lt;code&gt;:443&lt;/code&gt;, where T3 lives and the app doesn’t. Laravel needs the port in &lt;code&gt;ASSET_URL&lt;/code&gt;, as above. Hugo needs &lt;code&gt;--baseURL "https://$HOST/"&lt;/code&gt; with the scheme, so live reload picks &lt;code&gt;wss&lt;/code&gt; and its default &lt;code&gt;appendPort&lt;/code&gt; tacks the &lt;code&gt;:8809&lt;/code&gt; back on. Two frameworks, same debugging session.&lt;/p&gt;&lt;h3 id="every-agent-gets-its-own-worktree"&gt;Every agent gets its own worktree&lt;a href="#every-agent-gets-its-own-worktree" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;If you run more than one agent at once, they can’t share a working directory. Two agents editing the same files on the same branch trip over each other instantly. The fix is &lt;a href="https://git-scm.com/docs/git-worktree" target="_blank" rel="noopener noreferrer"&gt;git worktrees&lt;/a&gt;
: each agent gets its own checkout of the repo, on its own branch, in its own directory, all backed by the same &lt;code&gt;.git&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;T3 does this part for me. Start a new thread and it runs &lt;code&gt;git worktree add&lt;/code&gt; under the hood, so I can have four agents working on four features at once, each isolated on the filesystem. That much is automatic.&lt;/p&gt;&lt;p&gt;The reader who nudged me toward writing this had a fair point, though. In a tool like &lt;a href="https://conductor.build" target="_blank" rel="noopener noreferrer"&gt;Conductor&lt;/a&gt;
you press &lt;code&gt;+&lt;/code&gt; and get a new isolated workspace with its environment already set up. T3 creates the worktree, but the &lt;em&gt;environment&lt;/em&gt; inside it is on you. A fresh worktree is a bare checkout: no &lt;code&gt;.env&lt;/code&gt; (it’s gitignored, so it doesn’t come along), no installed dependencies, no running app. That’s what &lt;code&gt;worktree-up&lt;/code&gt; is for. It’s the glue Conductor bundles and T3 makes you supply: copy the &lt;code&gt;.env&lt;/code&gt; in, &lt;code&gt;composer install&lt;/code&gt;, start the preview. The catch is that it’s a &lt;em&gt;one-time&lt;/em&gt; cost. You write &lt;code&gt;worktree-up&lt;/code&gt; once per project (or, more realistically, let Claude build this once for you), flag it “run on worktree create,” and from then on every new worktree is a single click for me too. More setup up front, in exchange for controlling exactly what a worktree gets.&lt;/p&gt;&lt;p&gt;The isolation isn’t total, and the gap is easy to trip over. Each worktree gets its own files, its own branch, its own copied &lt;code&gt;.env&lt;/code&gt;, and its own preview server on its own port. What they &lt;em&gt;share&lt;/em&gt; is the database: &lt;code&gt;worktree-init&lt;/code&gt; makes one MySQL and ClickHouse database per project, and every worktree of that project points at it. Two agents on two branches see the same tables, and a migration one of them runs lands for both. That’s usually what I want for a quick feature branch, but if you need real per-branch data isolation you’d key the database name on the branch in &lt;code&gt;worktree-init&lt;/code&gt; rather than the project.&lt;/p&gt;&lt;p&gt;One more gap T3 leaves: there’s no “worktree removed” event, so nothing tears down the preview server when I delete a worktree. I handle that with a one-minute reaper (&lt;code&gt;worktree-gc&lt;/code&gt;) that drops any preview whose worktree directory has vanished. Not elegant, but a merged branch doesn’t leave a dead &lt;code&gt;php&lt;/code&gt; process and a bound port behind. That’s the whole thing, about twenty lines:&lt;/p&gt;&lt;p&gt;&lt;em&gt;Update: the reaper frees the port, but it never deletes the worktree directory, and each one carries its own &lt;code&gt;node_modules&lt;/code&gt;. That filled the disk a day after I published this. I wrote up &lt;a href="/deduplicating-git-worktrees-ext4-jdupes/"&gt;how I deduplicated the worktrees with jdupes&lt;/a&gt;
, and why copy-on-write would have been the better fix if the server weren’t on ext4.&lt;/em&gt;&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# worktree-gc, on a one-minute systemd timer. T3 fires a script on worktree&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# *create* but has no teardown event, so a merged or deleted worktree would&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# otherwise leave a server and a bound port behind.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;REG&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/.worktree-serve"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt; -d &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;exit&lt;/span&gt; &lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;shopt&lt;/span&gt; -s nullglob
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;for&lt;/span&gt; p in &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;/*.pid&lt;span&gt;;&lt;/span&gt; &lt;span&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;NAME&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;basename &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$p&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; .pid&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;PID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;cat &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$p&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span&gt;||&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;DIR&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;cat &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;.dir"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span&gt;||&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;DEAD&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;[&lt;/span&gt; -z &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PID&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; ! &lt;span&gt;kill&lt;/span&gt; -0 &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PID&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span&gt;;&lt;/span&gt; &lt;span&gt;then&lt;/span&gt; &lt;span&gt;DEAD&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;1&lt;span&gt;;&lt;/span&gt; &lt;span&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;if&lt;/span&gt; &lt;span&gt;[&lt;/span&gt; -n &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DIR&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;[&lt;/span&gt; ! -d &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DIR&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;then&lt;/span&gt; &lt;span&gt;DEAD&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;1&lt;span&gt;;&lt;/span&gt; &lt;span&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;[&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$DEAD&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;1&lt;/span&gt; &lt;span&gt;]&lt;/span&gt; &lt;span&gt;||&lt;/span&gt; &lt;span&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;PORT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;cat &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;.port"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span&gt;||&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;[&lt;/span&gt; -n &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PID&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt;  &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span&gt;kill&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PID&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span&gt;||&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;[&lt;/span&gt; -n &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; &lt;span&gt;]&lt;/span&gt; &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; tailscale serve --https&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;&lt;span&gt;$PORT&lt;/span&gt;&lt;span&gt;"&lt;/span&gt; off 2&amp;gt;/dev/null &lt;span&gt;||&lt;/span&gt; &lt;span&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;# (also tears down the worktree's docker compose stack, if it has one)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    rm -f &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;.url"&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;.port"&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;.pid"&lt;/span&gt; &lt;span&gt;"&lt;/span&gt;&lt;span&gt;$REG&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;.dir"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;    &lt;span&gt;echo&lt;/span&gt; &lt;span&gt;"reaped &lt;/span&gt;&lt;span&gt;$NAME&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="one-project-that-manages-the-server-itself"&gt;One project that manages the server itself&lt;a href="#one-project-that-manages-the-server-itself" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;One of the “projects” in my editor &lt;em&gt;is the server’s own admin repo&lt;/em&gt;. So from the same UI where I write code, I have a workspace whose job is administering the server: spin up a new project, install a package, tweak a config, check what’s running. The server can drive itself through the same agent interface as everything else. Think OpenClaw or Hermes, but for your own dev server.&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/t3_workspace_sysadmin.png" alt="The Box admin T3 thread, where the agent explains its own runtime on the dev box: the t3code systemd service, the tailnet-only URL it serves on, local env mode, and memory usage" width="1680" height="1428" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;Because that admin repo is tracked in git, every change to how the server works, the scripts above included, is reviewable in a diff and deployed by copying files out of the repo instead of hand-editing live files and hoping I remember what I changed. The server’s config is code.&lt;/p&gt;&lt;h3 id="the-details"&gt;The details&lt;a href="#the-details" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&lt;p&gt;A pile of little decisions to save you some time:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Anything that listens binds to &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/strong&gt; and is published to the tailnet with &lt;code&gt;tailscale serve&lt;/code&gt;. Nothing gets a public port. Be warned though: Docker’s iptables rules bypass the firewall entirely, so container ports need an explicit &lt;code&gt;127.0.0.1:&lt;/code&gt; prefix or they end up exposed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Databases are per-project and localhost-only,&lt;/strong&gt; so I can throw a real MySQL and ClickHouse at any project without ever exposing them.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id="a-whole-server-including-cron-jobs"&gt;A whole server, including cron jobs&lt;a href="#a-whole-server-including-cron-jobs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;The Remote Control comparison from the top only goes so far. This is a full Linux server that never sleeps, and that buys a couple of things a laptop-bound setup can’t.&lt;/p&gt;&lt;p&gt;It runs work on a schedule. The server is awake at 6am whether I am or not, so I let it earn its keep while I sleep. Some of that is ordinary cron: the nightly database dumps, the 05:00 tool updates from earlier. Better still, put an &lt;em&gt;agent&lt;/em&gt; on a timer. Claude Code runs headless with &lt;code&gt;claude -p&lt;/code&gt;, so a cron line or a systemd timer can hand it a chore:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code data-lang="bash"&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# 6am cron, while I'm asleep: bump deps and open a PR if anything moved&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt; &lt;span&gt;6&lt;/span&gt; * * *  &lt;span&gt;cd&lt;/span&gt; ~/projects/app &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude -p &lt;span&gt;"bump composer + npm deps, run composer audit, open a PR if changed"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;# Monday 7am: draft the week's changelog from the merged PRs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt; &lt;span&gt;7&lt;/span&gt; * * &lt;span&gt;1&lt;/span&gt;  &lt;span&gt;cd&lt;/span&gt; ~/projects/app &lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt; claude -p &lt;span&gt;"summarise last week's merged PRs into a changelog entry, open a PR"&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dependency bumps, a &lt;code&gt;composer audit&lt;/code&gt;, a summary of last week’s changes for the changelog, pre-provisioning a workspace so it’s warm before I sit down, a weekly sweep for dead code. None of it needs me at the keyboard, and none of it needs my laptop powered on. I wake up to a branch, read it over coffee, and merge or bin it.&lt;/p&gt;&lt;p&gt;It also runs my CI so my laptop doesn’t have to. Before I push and burn real CI minutes, I run the suite on the server. It can peg all its cores at 100% for a couple of minutes building assets and running tests, while my laptop stays cool and free for something else. The agent already lives next to the code, so it runs the suite and reads its own failures without shipping anything back and forth.&lt;/p&gt;&lt;p&gt;One note on all this scheduled work: the more you lean on it, the more a silently-broken job costs you, and a cron line that stops firing doesn’t announce itself. If you’re starting to rely heavily on this kind of automation on your dev server, consider monitoring those cronjobs with &lt;a href="https://ohdear.app/features/scheduled-task-monitoring?utm_source=ma.ttias.be&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blogpost-remote-coding-environment-vps" target="_blank" rel="noopener noreferrer"&gt;Oh Dear&lt;/a&gt;
too (my own product, so I’m biased, but this is exactly what it’s built for).&lt;/p&gt;&lt;h2 id="a-workspace-for-home-assistant"&gt;A workspace for Home Assistant&lt;a href="#a-workspace-for-home-assistant" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Not every workspace on the server is code. One of them is my house.&lt;/p&gt;&lt;p&gt;I run &lt;a href="https://www.home-assistant.io/" target="_blank" rel="noopener noreferrer"&gt;Home Assistant&lt;/a&gt;
at home, and it has an &lt;a href="https://modelcontextprotocol.io/" target="_blank" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;
server, the protocol agents use to talk to outside tools. So I gave the server a Home Assistant project whose MCP connection points at my house. Now, from the same T3 UI where I review a pull request, I can ask what the temperature is in the office, flip a switch, or check whether I left the garage open. From my phone, from anywhere.&lt;/p&gt;&lt;p&gt;The server can’t reach my home network, of course. It’s a server in a data centre, not a device on my LAN. The connection goes out through &lt;a href="https://www.nabucasa.com/" target="_blank" rel="noopener noreferrer"&gt;Nabu Casa&lt;/a&gt;
, Home Assistant’s cloud, which gives my instance a stable external URL. The MCP server on it holds a long-lived token and talks to that URL. From the agent’s side it’s just another tool: it can see all 2,196 entities my Home Assistant exposes, and act on them.&lt;/p&gt;&lt;p&gt;An always-on server that manages my home from a chat interface, reachable from my phone – that’s most of what those self-hosted assistants are for. I didn’t set out to replace one, but between the dev workspaces, the scheduled agents, and now the house, this server has become the always-on assistant I’d otherwise have installed OpenClaw to get.&lt;/p&gt;&lt;h2 id="monitoring-your-dev-server"&gt;Monitoring your dev server&lt;a href="#monitoring-your-dev-server" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This server went from a toy to something I lean on. My code lives here, my databases, the link to my house, the agents that run overnight. If it falls over, or starts leaking something I didn’t mean to expose, I want to know before I trip over it myself.&lt;/p&gt;&lt;p&gt;I monitor it with &lt;a href="https://ohdear.app?utm_source=ma.ttias.be&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blogpost-remote-coding-environment-vps" target="_blank" rel="noopener noreferrer"&gt;Oh Dear&lt;/a&gt;
, which I co-founded, so take this as the biased recommendation it is. Uptime is the obvious half: is the server answering. The more interesting half for a server like this is &lt;a href="https://ohdear.app/features/port-scanning?utm_source=ma.ttias.be&amp;amp;utm_medium=referral&amp;amp;utm_campaign=blogpost-remote-coding-environment-vps" target="_blank" rel="noopener noreferrer"&gt;port scanning&lt;/a&gt;
.&lt;/p&gt;&lt;p&gt;The security model here is “one public port, everything else on the tailnet.” That holds only as long as my firewall rules stay correct, and firewall rules rot. Docker punches straight through ufw. A &lt;code&gt;docker run -p 8080:8080&lt;/code&gt; I forgot to pin to &lt;code&gt;127.0.0.1&lt;/code&gt;. A package that helpfully opens a port when I install it. From the server itself it all looks fine, while from the outside there’s now a port reachable that I never meant to expose.&lt;/p&gt;&lt;p&gt;Port scanning checks the server the way an attacker would, from the outside. Oh Dear scans all 65,535 TCP ports on the public IP once a day by default, more often if you want, and you tell it which ones are meant to be open. On the first scan it shows you what it found and asks you to set a baseline. On mine that’s one port, 9999, the SSH break-glass door, and nothing else:&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/ohdear_portscanner_baseline.jpg" alt="Oh Dear’s port scanner baseline screen: one open port found, 9999 running OpenSSH, ticked as expected, with a Set Baseline button" width="1154" height="1127" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;From then on, every scan is checked against that baseline. As long as 9999 is the only thing answering, it stays green. The moment an unexpected port responds, I get a notification.&lt;/p&gt;&lt;p&gt;&lt;img src="/content/remote-coding-environment-vps/ohdear_portscanner_results.png" alt="Oh Dear’s port scan result reading All ports are as expected, with port 9999 the only open port, matching the saved baseline" width="1172" height="858" loading="lazy"&gt;&lt;/p&gt;&lt;p&gt;So the day I fat-finger a Docker flag and expose a database, I hear about it instead of reading about it in an incident report later. It’s the backstop for the exact mistake a tailnet-only server is one &lt;code&gt;docker run&lt;/code&gt; away from: “ah, I thought that was firewalled.”&lt;/p&gt;&lt;h2 id="hand-this-post-to-your-own-agent"&gt;Hand this post to your own agent&lt;a href="#hand-this-post-to-your-own-agent" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;I said at the top this doubles as a setup guide, and I mean it literally. This post is written to be a prompt.&lt;/p&gt;&lt;p&gt;If you have Claude Code, Codex, or a similar coding agent, you can hand it this entire article and say “build me this on a fresh Ubuntu VPS, here’s the IP”. The agent has enough here to provision the server, install Tailscale, lock down the firewall, set up the browser GUI, and wire in the auto-updates. That’s how I’d bootstrap a second one now: give an agent the write-up and a root login, and check its work as it goes. Verify each step rather than trusting it blindly, the same way I’d review a colleague’s pull request, but the bulk of the typing is done for you.&lt;/p&gt;&lt;p&gt;I set out to stop keeping my laptop awake, and I ended up with an environment I can rebuild just by describing it.&lt;/p&gt;&lt;p&gt;If you build your own version, or you’ve got a sharper take on the GUI layer, &lt;a href="https://twitter.com/mattiasgeniar" target="_blank" rel="noopener noreferrer"&gt;let me know&lt;/a&gt;
– I’d love to see what you come up with. New GUIs for this pop up weekly, chances are there’s a better one out by the time you read this.&lt;/p&gt;&lt;/div&gt;</description>
            <pubDate>Thu, 20 Aug 2026 18:55:44 +0000</pubDate>
            <guid>https://ma.ttias.be/remote-coding-environment-vps/</guid>
        </item>
        <item>
            <title>OpenScreenShot — Full-page screenshot &amp;amp; annotation tool for Chrome</title>
            <link>https://openscreenshot.app/</link>
            <description>&lt;blockquote&gt;&lt;hr&gt;&lt;/blockquote&gt;&lt;div&gt;
            
            &lt;p&gt;
              Full-page, visible-area and region screenshots for Chrome, with a complete editor that
              runs on your device.
            &lt;/p&gt;
            
            &lt;p&gt;Works on Chrome, Edge, Brave, Arc and other Chromium browsers.&lt;/p&gt;
            &lt;p&gt;
              &lt;span&gt;&lt;b id="stat-users"&gt;601&lt;/b&gt; Chrome Web Store users&lt;/span&gt;
              &lt;span&gt;·&lt;/span&gt;
              &lt;span&gt;&lt;b id="stat-stars"&gt;55&lt;/b&gt; stars on GitHub&lt;/span&gt;
              &lt;span&gt;·&lt;/span&gt;
              &lt;span id="stat-version"&gt;v0.6.0&lt;/span&gt;
              &lt;span&gt;·&lt;/span&gt;
              &lt;span&gt;MIT licensed&lt;/span&gt;
              &lt;span&gt;·&lt;/span&gt;
              &lt;a href="https://news.ycombinator.com/item?id=48839719" target="_blank" rel="noopener"&gt;Show HN thread&lt;/a&gt;
            &lt;/p&gt;

            &lt;div&gt;
                &lt;picture&gt;
                  &lt;source srcset="assets/hero.webp" type="image/webp"&gt;
                  &lt;img src="assets/hero.jpg" width="1940" height="1160" alt="The OpenScreenShot popup open over a web page, offering Full page, Visible area, and Region capture, with a region selection in progress." fetchpriority="high"&gt;
                &lt;/picture&gt;
              &lt;/div&gt;
          &lt;/div&gt;&lt;div id="how"&gt;
            &lt;div&gt;
              &lt;h2&gt;The whole workflow, in one place.&lt;/h2&gt;
              &lt;p&gt;
                Capture a full page, mark it up, blur what should stay private, then export — no tab
                juggling, no separate editor app.
              &lt;/p&gt;
            &lt;/div&gt;
            
            
            &lt;p&gt;Full-page capture to export, recorded at 2.4× speed.&lt;/p&gt;

            &lt;div&gt;
              &lt;div&gt;
                &lt;p&gt;&lt;span&gt;01&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;Capture&lt;/h3&gt;
                &lt;p&gt;
                  Full page, visible area, or a dragged region. One click in the popup, a keyboard
                  shortcut, or number keys 1–3.
                &lt;/p&gt;
              &lt;/div&gt;
              &lt;div&gt;
                &lt;p&gt;&lt;span&gt;02&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;Edit&lt;/h3&gt;
                &lt;p&gt;
                  Rectangles, arrows, lines, pen, highlighter, text, numbered step badges, blur or
                  mosaic or solid redaction, spotlight and crop — with undo all the way back.
                &lt;/p&gt;
              &lt;/div&gt;
              &lt;div&gt;
                &lt;p&gt;&lt;span&gt;03&lt;/span&gt;&lt;/p&gt;&lt;h3&gt;Export&lt;/h3&gt;
                &lt;p&gt;
                  PNG, JPEG, WebP, or multi-page PDF with page size and margins. Download it, or
                  copy straight to the clipboard.
                &lt;/p&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;&lt;div id="privacy"&gt;
              &lt;div&gt;
                &lt;div&gt;
                  &lt;h2&gt;Your screenshots never leave your browser.&lt;/h2&gt;
                  &lt;p&gt;
                    Screenshots are often the most sensitive images on your machine. The extension
                    makes no network requests at all.
                  &lt;/p&gt;
                &lt;/div&gt;
                &lt;ul&gt;
                  &lt;li&gt;
                    
                    &lt;span&gt;&lt;strong&gt;Image data stays on your device&lt;/strong&gt; — captures live in local
                      extension storage until you export them.&lt;/span&gt;
                  &lt;/li&gt;
                  &lt;li&gt;
                    
                    &lt;span&gt;&lt;strong&gt;No account, no analytics, no telemetry&lt;/strong&gt; — nothing to sign up
                      for and nothing to opt out of.&lt;/span&gt;
                  &lt;/li&gt;
                  &lt;li&gt;
                    
                    &lt;span&gt;&lt;strong&gt;Zero host permissions&lt;/strong&gt; — it reads the tab you capture, when
                      you capture it, and nothing else.&lt;/span&gt;
                  &lt;/li&gt;
                  &lt;li&gt;
                    
                    &lt;span&gt;&lt;strong&gt;MIT licensed, no watermarks&lt;/strong&gt; — read every line on GitHub,
                      keep every pixel of your exports.&lt;/span&gt;
                  &lt;/li&gt;
                &lt;/ul&gt;
                &lt;p&gt;&lt;a href="https://github.com/pghqdev/OpenScreenShot/blob/main/PRIVACY.md" target="_blank" rel="noopener"&gt;Read the full privacy policy →&lt;/a&gt;
              &lt;/p&gt;&lt;/div&gt;

              &lt;div&gt;
                &lt;p&gt;&lt;span&gt;manifest.json&lt;/span&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;"permissions": [
  &lt;span&gt;"activeTab"&lt;/span&gt;,
  &lt;span&gt;"scripting"&lt;/span&gt;,
  &lt;span&gt;"storage"&lt;/span&gt;,
  &lt;span&gt;"unlimitedStorage"&lt;/span&gt;,
  &lt;span&gt;"downloads"&lt;/span&gt;
],
&lt;span&gt;"host_permissions": []&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                &lt;div&gt;
                  &lt;p&gt;&lt;code&gt;activeTab&lt;/code&gt;&lt;span&gt;access the current tab — only when you click&lt;/span&gt;
                  &lt;/p&gt;
                  &lt;p&gt;&lt;code&gt;scripting&lt;/code&gt;&lt;span&gt;run the capture script on that tab&lt;/span&gt;
                  &lt;/p&gt;
                  &lt;p&gt;&lt;code&gt;storage&lt;/code&gt;&lt;span&gt;keep your settings and last capture on-device&lt;/span&gt;
                  &lt;/p&gt;
                  &lt;p&gt;&lt;code&gt;downloads&lt;/code&gt;&lt;span&gt;save exports to your Downloads folder&lt;/span&gt;
                  &lt;/p&gt;
                  &lt;p&gt;&lt;code&gt;host_permissions: []&lt;/code&gt;&lt;span&gt;no standing access to any website&lt;/span&gt;
                  &lt;/p&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;&lt;div id="download"&gt;
            &lt;h2&gt;Take a screenshot the way you meant to.&lt;/h2&gt;
            &lt;p&gt;Free, open source, and installed in seconds.&lt;/p&gt;
            &lt;p&gt;
              &lt;a href="https://chromewebstore.google.com/detail/hdabbojjccojlapnfjpdppcpfcnhgmdp" target="_blank" rel="noopener"&gt;
                
                Add to Chrome — Free
              &lt;/a&gt;
            &lt;/p&gt;
            &lt;p&gt;Works on Chrome, Edge, Brave, Arc and other Chromium browsers.&lt;/p&gt;
            
          &lt;/div&gt;</description>
            <pubDate>Sat, 15 Aug 2026 22:03:39 +0000</pubDate>
            <guid>https://openscreenshot.app/</guid>
        </item>
        <item>
            <title>ISO 24495-1:2023(en), Plain language</title>
            <link>https://www.iso.org/obp/ui#iso:std:iso:24495:-1:ed-1:v1:en</link>
            <description></description>
            <pubDate>Sat, 15 Aug 2026 05:20:25 +0000</pubDate>
            <guid>https://www.iso.org/obp/ui#iso:std:iso:24495:-1:ed-1:v1:en</guid>
        </item>
    </channel>
</rss>