The Quest for a Thin(ner) Client Development Experience
The Quest for Thin Clients
Basically everyone I know works on a laptop. When I was a kid, this was a luxury few could afford, and they were rarely purchased by individuals. But things have changed a lot in the intervening decades. Mobility is important to me, and most of the people I know.
I've been working remotely since 2011, always from a laptop. But my recently this has involved wrangling ever increasing amounts of data. Not to mention frequent compilation and long-running processing jobs.
Over the years, I've paid a significant Apple Taxβ’. While my M1 Max is fantastic (unquestionably the best personal computer I've ever owned from a hardware perspective), absolutely nothing is upgradeable. 32GB of RAM is plenty 99% of the time, but only getting 1TB of storage years ago was a huge mistake. And given the rise of agentic development, it's increasingly useful to run things in parallel.
Mulling this over for the past few months has led me back to my long-term fascination with the idea of thin clients. The idea of separating the graphical terminal from the heavy compute and RAM goes back decades to when portable computers were mostly a pipe dream, and a client/terminal was something installed at a desk or tied to a room. Networking speed and reliability have advanced a lot since then, so theoretically, we should be able to shift compute and storage back to bigger, upgradeable workstation/server-class hardware, and pay less for laptops every few years.
In a manner of speaking, most consumer devices today are already much more "thin" than we realize. In the '90s, if you wanted to play music or watch a video, you needed to have the files available locally (on your hard drive or disc usually). Nowadays we usually stream it over the network without even thinking. Documents and spreadsheets have been available in the browser for over a decade (and are often better than the locally running software). And even creative tools like Photoshop are no longer tied to the local computer.
But software development has been somewhat resistant to this, due to complexity of the workflows. Particularly when things like a mobile device simulator or a debugger are involved. While I have some good friends that use git from the CLI and eschew LSPs, I'm far more productive with more modern tooling. But that too tends to be designed for running locally with a GUI.
Things are changing though! I won't say I've completely cracked it yet, but over the summer I worked out a surprisingly comfortable "thin client" development setup. My MacBook is now essentially a graphical interface, and the heavy processing and storage are almost entirely offloaded.
I'm here today to talk about the components of a setup that works for me. Hopefully this gives some inspiration if you're considering something similar.
The Terminal
I'm far from being a terminal maximalist (in fact, I think they are a terrible UX for a lot of things), but I still spend a ton of time in them. And they are the most straightforward part of externalizing your dev setup.
Terminal Emulator
Terminals have historically had poor support for Unicode. You can pretty much forget bidirectional text support or mixed fonts. And the landscape varies widely in terms of handling for scrollback and an increasingly fragmented set of standards (image display, clipboard, etc.) that TUIs tend to leverage.
I am currently using GhosTTY, which is probably not a very surprising thing to say in 2026. It doesn't magically solve everything, but it's the first project in a while to significantly improve the status quo. The first releases were pretty rough, so I bounced back to iTerm2 the past few times I tried it. But it's now to the point where I only need one or two small edits to my config to be happy.
My primary remaining complaint with GhosTTY is with everyone else's computers ;) Most Linux distros (and even the most recent OpenBSD 8.0 release!) don't have updated terminfo yet, so you'll need some workarounds on basically every Linux box you connect to.
One of the other things I really appreciate about GhosTTY
is that the team also put in a lot of effort to extract the core
into libghostty.
This is directly responsible for several unrelated applications getting better embedded terminals
(for example, ghostel inside Emacs).
Multiplexers
If you've ever started a long-running job, you probably learned the hard way what happens when your SSH connection is interrupted. The primary thing multiplexers provide session persistence so you don't lose your work.
All multiplexers have quirky defaults which will probably annoy you. Most of them have tiny scrollback buffers by default for some reason which eludes me given how much RAM a typical developer machine has these days. It seems they could spare a little on some text. Most multiplexers also have key bindings that conflict with something else. Which is really baffling to me since they are ostensibly designed by and for heavy terminal users!
I currently use Zellij because it makes shortcuts discoverable in the TUI, and the scroll wheel actually works out of the box (which can't be said for like 99% of multiplexers).
I've set up shell aliases that connect to a well-known session name on my remote dev system.
# tmux version
mosh devcube -- tmux new -ADs mosh-session
# zellij version (Zellij has no switch for auto-detaching other sessions FWIW)
mosh devcube -- zellij attach -c mosh-session
I should also point out that at the time of this writing, I'm aware of at least 2 promising projects which are looking to shake up the multiplexer space. Those are Rex from Superlogical and zmx. Rex is a full-on terminal emulator replacement designed as a "multiplexer for everything." It's in early invite-alpha and I just received my invite yesterday. Zmx comes highly recommended from some friends as a lighter alternative to the likes of Zellij and tmux, focusing only on the session persistence. I have not had a chance to try either of these yet but watch this space and I'll be sure to write when I do.
Mosh
Notice that I didn't use ssh but rather mosh.
Multiplexers can address some of the paper cuts,
but SSH requires the server to echo every keystroke,
more or less synchronously.
Even if it's a multi-thousand line compiler error.
And if you shut your laptop at home and open it 5 minutes later on the bus,
your terminal will probably be unhelpfully frozen.
Mosh works around many of these issues. It predictively echoes keystrokes, and corrects as needed. It's really good at this, and even understands applications like Emacs! And it keeps track of the correct state and synchronizes that, rather than forcing every character from the PTY to be echoed back. And since it's really a synchronization protocol that runs over UDP rather than stateful TCP connections, it supports roaming between IP addresses (reconnects are magically fast). Pair this with a multiplexer and you have a setup that's almost as good as sitting at the console.
Mosh is fantastic, but it lacks several really important features that I regularly with SSH. The most consequential one is agent forwarding. I use this all the time so I don't need to provision new SSH identities all over, or (even worse) copy an existing one.
The Guardian Agent project was designed to address this, but it was experimental on release, and hasn't been maintained in about 9 years.
Service port forwarding
I frequently need to forward ports to the remote dev environment. For example, to access the development web server or a database from my laptop.
SSH port forwarding is a great tool for this. But just like regular SSH connections, it doesn't gracefully handle connection drops. But we can do a few things here to improve the user experience.
First, I always tune a few things in my ~/.ssh/config:
Host *
ServerAliveInterval 5
ServerAliveCountMax 3
This aggressively tries to keep sessions alive, and detects dropped connections pretty quickly. I pair this with autossh, a simple tool that monitors SSH connections and automatically reconnects when they drop.
autossh -M 0 -N -T -o ExitOnForwardFailure=yes -L 5432:5432 devcube
This is good enough for most service port forwarding use cases.
Tailscale
If you'll only ever talk to other computers on your LAN or on the public internet, then you could skip this section. But if you want to access a machine in your home office from outside, or access a work server behind the office firewall from outside, you probably need a VPN of some sort.
When the remote box isn't publicly reachable, Tailscale is a great solution. It's easy to set up, free for most use cases, and can almost always coordinate a DIRECT connection between you and a server on your VPN ("tailnet")! And the client software is very good at persistently reconnecting when you switch networks.
Conceptually, Tailscale is WireGuard + NAT hole punch coordination + optional "Magic" DNS (so you can easily find your other devices). You can self host a coordination server if you want via Headscale, but I don't see any particular need for most people.
30km away on the other side of town, my ping to the servers at home is only about 10ms! And when I'm on the same network, ping is around 1ms. There's a slight overhead, but it's so small that I always use the "magic" hostnames. This way I don't have to think about whether I'm home or not. Any programs that had a semi-persistent connection (like mosh or Emacs TRAMP buffers) automatically reconnect.
It's hard to overstate how great the UX is. I've tried half a dozen "remote" development setups over the years, and "seamless" is not a word I'd have used to describe many of them.
Remote Development Environments
Coming back to the core use case (which I've so far danced around a bit), let's focus on the development experience. JetBrains, VS Code, Zed, and Emacs all offer varying levels of this, where you interact with a full graphical application locally, but the storage, compilation, and execution happen remotely.
JetBrains?
I was a JetBrains user since the very early days of PyCharm, and up until recently had some mix of RustRover, DataGrip, CLion, and Android Studio open most of the time. When they announced their early previews of what was then called "projector," I was ecstatic! It sounded perfect. Run a little server on the remote box, and your favorite IDE would deploy itself, rendering most of the frontend locally and offloading the heavy stuff.
This unfortunately, several years later, is still pretty disappointing. It's super finnicky, doesn't work well on many platforms, and requires Java on the remote. They appear to use basically the same technology for their dev containers implementation now. But even locally against a Docker container, it's barely usable. Major bummer π
VS Code or Zed?
VS Code's implementation of dev containers is significantly better, and they've invested a fair bit into getting SSH-based remote development working well (or so I've been told). Zed has an extremely similar workflow, which I did test drive a bit for SSH development.
Both VS Code and Zed have essentially the same architecture: deploy a small server on the remote, over SSH, and then set up a connection to that. As we already discussed, SSH's design is basically synchronous, so it'd be unusably slow as-is, but the server works around this.
I don't particularly like VS Code myself, but I know many others with different opinions. If you like VS Code already, this is worth a look. However, both VS Code and Zed lack (good) support for a lot of platforms. If you're sticking to macOS and Linux, this isn't an issue.
Emacs
When re-evaluating my choice of development environment recently, I gave Zed a really hard look. It's come a LONG way in the past year or so, and it's actually (sometimes significantly!) more performant than Emacs. But I eventually settled on Emacs for a few reasons:
- Emacs works everywhere; Zed is clearly prioritizing macOS and (to some extent) Linux.
- The Zed plugin system is quite limiting. This is partially just a good opinionated design, but you can't build anything like Magit or Org mode, and you definitely couldn't configure a workflow improvement like this.
Seamless Access with TRAMP
TRAMP is probably the coolest feature of Emacs.
File paths can contain "methods" (not unlike a URL scheme),
and Emacs has dozens of these (e.g. ssh) built in.
These can represent things beyond a transport protocol,
like "escalate to root access with sudo"
or "enter this podman container."
This is significantly more powerful than the capabilities most editors have.
You can even chain multiple methods together,
like /rpc:pf.home.arpa|sudo::/etc/pf.conf!
I've never seen anything like this in another editing environment.
And besides the dozens of built-in methods,
you can build your own.
The built-in ssh method is very... conservative,
which means it probably still works with some mainframe from the 1970s,
but it's also really inefficient for modern development with eglot, Magit, etc...
The community ended up settling on the same architecture as the Zed / VS Code server as a faster alternative. I wrote about this in a previous post.
Emacs + TRAMP-RPC + Tailscale = π₯
Emacs as a base is probably the most flexible editing environment ever created. Combine with TRAMP-RPC for remote access, and Tailscale for a persistent hostname to connect to, and you get the ultimate remote-ready development environment. It doesn't matter whether "remote" is halfway around the world, across the office, or even in a container on your machine.
TRAMP "just works" with most Emacs packages automatically. Though a few reequire a TRAMP add-on to work well (it's not a perfect system after all, but it is extensible). For example agent-shell needs the agent-shell-tramp package. Others like Ghostel or Magit will "just work."
The most important thing to me as that it's seamless and doesn't require changes to my workflow, regardless of local or remote development. This is especially important as a professional if you work with coding agents. Since it's impossible to trust the "sandboxing" of the vendor tools, I'm almost always inside some sort of container (usually a FreeBSD jail). I've found it's easier to just always work inside a container, regardless of whether I need the "sandbox" or not. The clean, predictable environment alone is worth it, and by always using this (and removing certain kinds of tools from the local environment), I've created a pit of success that encourages good hygiene.
My only wishlist item at this point is for TRAMP-RPC to add some sort of session roaming, like Mosh. That way I could shut my laptop, hop on a bus, and my agent-shell sessions would resume ;) But for most things (regular file buffers, Magit, etc.) this isn't a noticeable paper cut.
Also, not directly a TRAMP-RPC problem, but eglot sometimes gets janky if you're roaming around. Some internal state will occasionally get stuck or corrupt when changing networks. I developed a workaround for this in elisp.
Large File Sharing
With the thin development workflow sorted, there's still one more thing that occasionally comes up: working with large files. For example, I may be working on a data pipeline that outputs a few hundred gigabytes of GeoParquet files. While I could use tools like DuckDB directly on the remote dev box in some cases, sometimes I want to inspect them visually with tools like QGIS. Ideally without copying the entire file over to my cramped laptop hard drive.
I haven't found a great solution for this yet to be honest. I've tried Samba and NFS.
NFS on macOS is a disaster. It's configuration is obscure, poorly documented, and totally out of the realm of normal system configuration. My first and final attempt resulted in a kernel panic, which I hadn't seen in a minute.
Samba works, most of the time, but is littered with bugs. Besides being quite slow at directory listing, even over a LAN, there is an ongoing serious issue where Samba on macOS is caching the contents of files. I can understand caching directory contents for a short period, and even caching contents to some extent, but it's completely opaque and the issue can persist for minutes at a time. Apple seems to have little interest in maintaining their implementation, and I won't be surprised if they drop it in the near future.
If anyone else has better ideas, hit me up on Mastodon.
How it's going
I've been using this as my daily driver setup for over 2 months now. When working at home against a remote box on the same network (over Tailscale, but looping back in), it's practically indistinguishable from developing locally.
Within South Korea, even on a mediocre cafe WiFi connection, it's almost indistinguishable from LAN development inside Emacs. There are occasional tells depending on the quality of the network, but not enough to cause serious inconvenience while out at a coworking session.
I even tried this across the sea while in Japan last month. Whether tethering through my phone or using bad hotel WiFi, I was still able to work with relatively minimal disruption!
Overall, I'd say if you have a stable ping of less than 100ms or so and reasonable throughput, you won't notice much of a difference between local and remote development in Emacs with TRAMP-RPC. Shell-based workloads with Mosh are significantly more fault-tolerant.
The primary persistent issues I have are related to the way that much of Emacs is single threaded.
This becomes occasionally problematic during an extended network interruption
(e.g. when roaming without WiFi, Emacs will freeze until I throw it a few C-gs, even if I'm focusing on a local buffer).