Skip to content
all articles

My coding agents live on a headless VM, and my phone is the remote

Daniel Dewhurst13 min read

How I run T3 Code on a Proxmox VM with no screen, reach it over Tailscale from a MacBook and an iPhone, and treat the thread list as an inbox.

The agent that researched this post ran on an Ubuntu VM in my house. The VM has no monitor, no desktop and no GPU. I read its findings on a MacBook, and I could have read them on my phone. Nothing in the workflow needs the laptop to do the work.

The software in the middle is T3 Code, the open-source app from Theo Browne and Julius Marminge. I also use it at work, which is where most of my usage is. That runs on another machine and its data stays there, so everything below comes from my personal setup: one VM, one Mac and one iPhone.

A control surface, not a harness

T3 Code’s FAQ opens with the question “Is T3 Code a harness?” and answers no. A harness is the thing that turns a model into an agent: Claude Code, the Codex app server, Cursor’s CLI. T3 Code drives those. It starts the CLIs you have already installed and logged in to, so you bring your own subscription, and the README calls it an “agent harness control surface”.

Under the apps, a Node server owns the provider processes, terminals, git and project files. The desktop app, the web app at app.t3.codes and the iOS and Android apps all talk to that server over an authenticated WebSocket. The architecture doc is strict about this: a remote client “must never substitute its own filesystem, provider credentials, or machine state for the environment’s”. The desktop app bundles a server, but nothing forces you to use it.

So the server can live anywhere. Theo says in the FAQ that less than 5% of his prompts run on his laptop.

It is MIT licensed, has about 41,500 stars and moves fast: it merged roughly 4,480 pull requests in the 30 days before I wrote this. v0.1.6 came out on 31 October, with nightly builds several times a day after it.

The box

The VM runs on Proxmox on a server at home. It has 8 vCPUs, 16 GB of RAM, a 100 GB disk and Ubuntu 24.04. Before T3 Code, I drove agents on it over SSH and tmux, from the Mac or from the phone.

That worked, with a few hacks. The one I’m happiest to retire is image paste. Claude Code on Linux reads the clipboard through xclip, and a headless VM has no clipboard. I wrote a shim that answers Claude Code’s two xclip calls from a port that SSH forwards back to the Mac. It works, but only while an SSH session from the MacBook is open. From the iPhone, there’s no forward and no image.

T3 Code handles attachments on the server side. An image you attach is saved as a file on the server, outside the project. In full-access mode the agent can read that file, so “add this screenshot to the PR” works. It doesn’t matter which device you pasted it from.

Putting it on the tailnet

I installed T3 Code on 13 October as a systemd user service. The current instructions use the install script and then t3 service install:

bash
curl -fsSL https://t3.codes/install.sh | sh
t3 service install

On Linux, the background service doc says setup “enables lingering so T3 Code starts at boot and keeps running after logout”. The unit is t3code.service, and t3 service status shows where the logs are.

A new service only answers on the machine itself. I made that explicit, because I want the only way in to be Tailscale:

bash
# ~/.t3/service.env
T3CODE_HOST=127.0.0.1

Then t3 service restart. Tailscale Serve sits in front of it and gives the server an HTTPS address on the tailnet. T3 Code can set that up for you when it pairs a device:

t3 pair --tailscale --label iphone

That prints a pairing link with a one-time token, valid for five minutes by default. It also creates a Tailscale Serve mapping on port 443 that survives restarts. Open the link on the device and it keeps its own session from then on. On a headless box, you manage devices from the shell:

bash
t3 auth session list
t3 auth session revoke <id>

Two more commands made the setup work properly. Ubuntu 23.10 and later block the sandbox that headless Chrome needs, which breaks the built-in browser and HTML previews. sudo t3 browser setup adds an AppArmor profile and installs the missing libraries. Updates are t3 update, which switches both the t3 binary and the service to the new release. It asks before it restarts, so I use --yes.

HTTPS isn’t optional here. The hosted web app at app.t3.codes can only connect to an HTTPS endpoint, so a plain http:// LAN address won’t work from it. Tailscale Serve gives you a certificate for free.

My server now has two live sessions: the Mac desktop app and the iPhone app.

Why not T3 Connect

T3 Code has its own answer to all of this. T3 Connect runs cloudflared on your machine and uses a relay to set up the link. The relay doesn’t proxy your HTTP or WebSocket traffic, which goes through the tunnel. You sign in, run one command, and the machine appears in your apps with no VPN on the phone.

I already had Tailscale on every device, and I’d rather not add a second way into the VM. There’s also a limit: three machines per account (phones and browsers don’t count), raised to 10 or 100 if you link a paid T3 Chat plan. The FAQ says Connect is free “for now”. Theo writes that he uses Tailscale as a fallback himself.

The cost of Tailscale is easy to miss. The notifications doc says: “Background delivery requires T3 Connect; a direct or Tailscale connection alone does not enable push notifications.” On Tailscale alone, my phone doesn’t tell me when a thread needs me. I can live with that, because the inbox shows me what needs me as soon as I open the app.

The VM is the sandbox

Every thread on my VM runs in full-access mode. In the process list, Claude Code starts with --permission-mode bypassPermissions, and T3 Code puts each agent in its own systemd scope with a high OOM score, so the kernel kills an agent before it kills the server.

That level of access is the reason the agents live on a VM. If one of them deletes the wrong directory, the damage stays on a VM that I can rebuild from a template, not on my Mac. It isn’t harmless: the VM holds a GitHub SSH key and a gh login, so an agent can push. I can revoke both from GitHub. It has an SSH key for one other host, and none for the Proxmox host or the other VMs.

The auth model doesn’t give you much more protection than that, and its docs say so. A paired session gets every scope by default. That includes the terminal, which is a shell, and orchestration, which can start a full-access agent, so the docs argue that holding back the management scopes “would not be a boundary”. filesystem:read can read any file the server’s user can read, not only project files. “Projects are organizational boundaries, not filesystem sandboxes.” You can narrow a token with --scope when you pair, but the realistic boundary is the machine. Treat a paired phone like an SSH key to that box.

There is also t3 service protect, which reserves memory so runaway agents can’t starve the server. It offers to protect Tailscale and SSH as well. I haven’t run it.

The sidebar is an inbox

This part has nothing to do with remote access, but it matters more when you check in from a phone.

Most agent apps show a list of recent conversations. T3 Code’s sidebar has sections: Pinned, Active, Working, Snoozed and Settled. In a video he posted in late October, Theo puts the rule simply: “Every time you open a thread, the goal should be to close it.” Closing can mean the PR merged, the question was answered, or the work isn’t needed any more.

Instead of archiving a thread, you settle it. Settling is one click, and Undo stays on screen for five seconds (Cmd+Z also works). Snooze moves a thread out of the way until a time you pick, and it wakes early if the agent asks a question or fails. The Working section, a beta that’s on by default, holds threads that are still running, so the active list only shows threads waiting for you.

The part I rely on most happens without me. The server settles threads for you, even when no app is open:

  • A thread settles when its linked PR merges, as long as the merge happened after your last message.
  • A thread settles after three days with no activity. You can change this, between 1 and 90 days, or turn it off.
  • Pending questions, approvals, running work and active snoozes all block auto-settle, and you can disable it for one thread.

The merge rule does more work than it sounds. My global instructions tell agents to open a PR, start three review subagents, wait for CI and the review bots, fix the findings and merge. When that merge lands, the thread settles itself, and I never have to clear it.

What my inbox holds

The database on the VM has 439 threads, almost all from 29 October to 2 November. I created 54 of them, agents created 383, and the server created two. Most of the agents’ threads are child threads, which another thread started to delegate part of its work.

Of my 54, I started 25 from the iPhone and 29 from the Mac. I do everything from the phone: start threads, answer questions, read diffs and merge. The phone is close to half of the work I start, and it’s a small screen. A list of 439 conversations would be useless on it.

347 of the 439 threads are settled. 102 threads link to at least one PR, 141 links in total. I have never snoozed or pinned anything.

On the phone, the inbox is the whole interface. You swipe to settle or snooze, and the list in front of you is the list of things that need you.

Cmd+Enter fits the same model. It sends the prompt as a new thread in the background and opens a fresh draft, so you can fire off three tasks without watching any of them start. Each one gets its own git worktree if you choose that, and my worktrees take up 2.7 GB. Worktree cleanup is off by default. I set mine to remove a worktree when its PR merges, or after eight days.

Where the inbox gets it wrong

Not everyone likes this, and some of the complaints are fair.

When the new sidebar shipped, a GitHub discussion titled “Keep the legacy sidebar a supported option” collected the case against it: rows that fit too few threads, slower navigation, and no grouping by project. Theo’s account closed it on 28 October and pointed to a Labs option called “Boring sidebar (group by project)”. Theo says in the video that he didn’t think people would like it, and he shipped it behind flags at first.

The Working section had a sharper problem. Until late October, it dropped a thread when only a background command was still running. One user wrote that “the illusion a session is done when it is not” put them off the app. Since v0.1.3, a command that the agent says it is waiting on keeps the thread in Working. A dev server that the agent leaves running still doesn’t.

Switching harness mid-thread

T3 Code lets you change the provider in the middle of a thread, from Codex to Claude for example, and Theo shows this off in the video. I haven’t used it on this VM. Every one of the 439 threads ran on Claude, so take this section as what the docs say rather than what I’ve seen.

The user docs describe the handoff as a budgeted selection of real messages with their roles, plus command outcomes, capped at 16,000 tokens by default (T3CODE_CONTEXT_HANDOFF_TOKEN_CAP). It isn’t an agent-written summary, and the new agent can read the rest of the history with a tool. It doesn’t carry the old provider’s reasoning, tool state or attachments. Switch back to a provider you used before and, by default, T3 Code resumes that provider’s own session and adds what it missed. If the resume fails, it starts fresh with a handoff.

An internals doc describes text items cut to 240 characters and says handoffs “do not claim native provider context parity”. The two docs don’t agree on how much text survives. The safe reading is that a switch gives the new model a good brief, not a memory.

What it costs

T3 Code is free and MIT licensed, and there’s no way to pay for it. Some costs still exist:

  • Your own compute. Something has to run the server and stay on. The phone is only a remote.
  • Claude limits. T3 Code drives Claude through Anthropic’s Agent SDK, and the FAQ says that “will drain your 5h limits faster than using Claude Code directly”. It blames a bug on Anthropic’s side. The issue has more than 70 comments. The other cost is the obvious one: you run more threads, so you use more tokens.
  • Telemetry. It’s on by default, and sends product usage events such as provider, model and token totals. The FAQ says no prompts or file contents. This was a sore point, because the site once said there was no telemetry unless you opted in. The opt-out is an environment variable, T3CODE_TELEMETRY_ENABLED=false, which goes in the same service.env. Mine doesn’t set it, so my telemetry is on.
  • Uptime. There are reports of the server’s memory growing until it crashes after a day or two on headless Linux. Mine has been up since 31 October and sits at about 700 MB.
  • Pace. Nightlies arrive several times a day, and some releases break things for a while. The stable channel is calmer.

If you want the same setup

  • Use a VM or a spare box that you’d be happy to wipe, because the agents will run with full access.
  • Install the service, then put T3CODE_HOST=127.0.0.1 in ~/.t3/service.env so nothing on the LAN can reach it directly.
  • Pair each device with t3 pair --tailscale --label <name>. Check what’s paired with t3 auth session list now and then.
  • Remember that Tailscale Serve opens the server to every device on your tailnet, and only the pairing stops them. If you share your tailnet, restrict the VM’s port 443 to your own devices with a Tailscale access rule.
  • On Ubuntu, run sudo t3 browser setup before you wonder why previews are blank.
  • Decide about T3CODE_TELEMETRY_ENABLED and t3 service protect when you set it up, not three weeks later.
  • Turn on worktree cleanup, and leave auto-settle on.
  • If push notifications matter to you, use T3 Connect, with Tailscale as the fallback.