I was already coding with agents. Claude Code in one terminal, Codex in another, both of them grinding through work while I read diffs and drank coffee. That part was fine. That part was great, actually.
The problem was where they lived.
Everything ran on my laptop. Every session, every worktree, every half-finished refactor sat on a machine I carry around in a bag. Close the lid and the work stopped. Get on a train and the work stopped. Let the battery die in a cafe and whatever the agent was in the middle of just evaporated. The agents were not the bottleneck. My laptop's power state was.
So I got a VPS.
I opened Claude Code and said: "I just got a VPS. I want to log in and set up everything. How do I do it?"
Six hours later I had a machine that does not care whether my laptop is open.
Domenic showed the way
I should say: none of this was my idea. Domenic wrote the blog post that mapped this territory. His setup uses a home VM behind NAT, Tailscale for the VPN, git worktrees for parallel agents, and portless for dev server previews. Mine adapts all of that to a cloud VPS instead of a home machine, which changes the security surface significantly. But the architecture is his. Credit where it is due.
The thing about Domenic's post is that it reads like a blueprint. You can hand it to an AI agent and say "build me this, but on a VPS" and it actually works. That is what I did.
Tailscale was already there
I expected to spend an hour setting up Tailscale. Instead, Claude discovered it was already running. The box was already on my tailnet with its own IP address. Someone (probably me, probably during a late-night experiment I do not remember) had installed it already.
So we just needed to configure it properly: HTTPS certificates for secure dev server previews, SSH access through the tailnet, Taildrive for mounting remote folders in Finder. The HTTPS cert provisioned for the machine and suddenly every dev server I started could get a real secure context URL, reachable only from my devices.
This was the first "wait, can I actually talk to this from my phone?" moment. Yes. Yes I can.
Worktrees with personality
Claude set up a shared worktree directory at /home/agent/worktrees/. Both Claude Code and Codex write their isolated worktrees there. The naming convention is what makes me smile every time I list the directory.
Instead of UUIDs or session IDs, each worktree gets a memorable slug: two random words that sound like trail names in a national park. brave-otter, gentle-gulch, vivid-knoll, bold-cinder. I can look at my worktree listing and know which one is which without decoding a hash.
You can also name one yourself: set AGENT_WORKTREE_NAME="fix-login-bug" and that is what the directory gets called. The hook sanitizes it automatically: "Fix Login Bug!" becomes fix-login-bug. Duplicates get rejected with exit code 1 rather than silently attaching to the wrong tree.
This naming system is genuinely useful. I come back after lunch, see brave-otter and quiet-harbor sitting side by side, and I know exactly which agent is working on what. No cognitive overhead.
The root problem
An hour in, I hit the wall. Claude Code refuses to run --dangerously-skip-permissions as root. The error is explicit: "cannot be used with root/sudo privileges for security reasons." This is the right call by Anthropic, but it meant everything I had set up under /root was useless for autonomous agent work.
So I migrated everything to a new agent user. Created the account, rsynced all the configs, moved chezmoi, repointed the worktree hooks, retargeted Taildrive shares, reinstalled Hermes. Claude handled the whole migration. It even caught the things that broke silently: the hook still hardcoded /root/worktrees, the .codex/worktrees symlink pointed to a dead path, the backup cron was running under the wrong user.
The agent account got passwordless sudo and docker group membership. That makes it root-equivalent by two independent paths. It satisfies Claude's check, but it buys no actual isolation. I am fine with that. This is a dev box, not a bank vault.
14 repos, 34 MCP servers
By this point the box had fourteen repos cloned and configured. The MCP situation was worse. I had 17 servers showing up on the VPS but 34 in my local setup. Claude tracked down three separate causes: the .mcp.json file never made it onto the box, agents sync was silently skipping because it thought it was current, and mcp-server-fetch had an upstream bug where McpError was renamed to MCPError in SDK 2.x. Fixed all three. 34 servers, 27 connected.
The backup that caught its own leak
One of the first things I asked for was a backup system. Chezmoi syncs configs to a private GitHub repo with age encryption for secrets. A cron job runs devenv-snapshot every night, and a bootstrap script can rebuild the entire machine from scratch.
The system includes a leak scanner. It greps every staged diff for Postgres URIs, AWS keys, and PEM blocks before the push goes through. If it finds anything, it aborts. No push happens.
Then, during a routine sync, the scanner fired. It had caught something real: earlier, Claude had added ~/.hermes/config.yaml to chezmoi as plaintext, and that file had resolved secrets baked in. A New Relic key, a Grafana token, a Postgres URI. Three live credentials. Would have been pushed to GitHub in the clear.
The scanner was doing exactly what it was built for, catching a mistake Claude itself had made. I have never been prouder of a piece of defensive scripting.
Headroom, the proxy that almost broke everything
Headroom is a context-compression proxy. It sits between Claude/Codex and the API, compressing payloads before they reach the model. Less context, fewer tokens, lower cost.
Installing it was supposed to be headroom init. What actually happened:
Claude started a test proxy. Headroom's own install command started a managed proxy on the same port. Port conflict. The test proxy failed to bind, but Claude's test still reported success because the managed one was already running. Then Claude tried to clean up with pkill -f "headroom proxy" and killed its own SSH session.
Three times. Claude killed its own session three separate times with overly broad pkill patterns. Each time I reconnected and it picked up where it left off, slightly more embarrassed.
Eventually it moved the test commands into script files so the process names would not match the kill patterns. Both Claude and Codex now route through Headroom. The proxy is a systemd user service that survives reboots.
Agents sync also wipes Headroom config (same failure mode as yolo mode). Claude caught this and built a codex-postsync wrapper that restores everything after each sync. The kind of defensive engineering you only build after getting burned once.
Hermes: the receptionist, not the architect
I wanted Slack integration. Message a bot, an agent does the work, results come back to me. Hermes was already running on the box, connected to Slack. The gateway was crash-looping (243 restarts, a duplicate systemd unit), and the Discord token was invalid, but Slack worked.
The pipeline: Slack message hits Hermes, Hermes dispatches to Claude Code via claude -p, Codex reviews the diff independently, results come back via hermes send. The Claude-builds/Codex-reviews split actually matters because the reviewer is a different model that did not write the code.
Claude's advice was to treat Hermes as a dispatcher, not a decision-maker. Parse the Slack message, run the wrapper script, format the reply. Do not let the orchestrator decide what to build. DeepSeek V4 Pro is capable, but the frontier reasoning should happen in Claude Code and Codex.
Portless and the three self-kills
tportless gives each dev server its own Tailscale URL. Run tportless in a worktree and you get a dedicated tailnet URL pointing at that specific app. A second agent gets the next port, a third gets the one after that.
When I first asked about it, Claude admitted it had installed the wrapper but never tested it. And it was broken. tailscale serve needs --operator=agent to work without root, and that flag was missing. The proxy started and printed a localhost URL so it looked like it worked, but the tailnet URL silently never registered.
During testing, pkill -f killed the SSH session again. And again. Three total self-kills across the whole session. Same mistake, same pattern, same sheepish reconnect each time. Claude eventually created proper test scripts instead of running commands inline, so the patterns stopped matching.
The fix was one line: sudo tailscale set --operator=agent. Then we verified with two concurrent apps on different ports, each returning its own content. That is the property that matters for parallel agents.
The WebUI
The final piece: Hermes WebUI, a browser interface for the Hermes agent. It defaults to the same port Headroom already claimed. Claude spotted the collision before it happened and pinned the WebUI one port over.
Now it sits behind a dedicated tailnet URL on its own port, password-protected. Three systemd user services survive reboots: hermes-gateway, headroom-default, hermes-webui. All under the agent account with linger enabled.
The server change protocol
Claude also wrote a skill called server-change-protocol. Every package install, config change, firewall rule, or cron job must be documented in Notion, committed to the developer environment repo, and verified. No exceptions. A change that only exists on the running server is a change that dies with the server.
This is how infrastructure should work. Not hope, not memory, not "I think I installed that." A reproducible box that can be rebuilt from a single bootstrap script and a git repo.
What this actually enables
I can code from my phone on a train. I message Slack with a feature request, Claude Code picks it up in a worktree, Codex reviews the diff, and the result lands in my inbox. If a production bug surfaces while I am away from my desk, I do not need to find a laptop and wifi. I type a message and an agent fixes it.
The agents run in parallel, each in its own isolated worktree, each with its own tailnet preview URL. Nothing collides. Nothing gets confused about which branch it is on. And the lid problem is gone. I close my laptop, the tmux sessions keep running, the agents keep working, and when I open it again everything is exactly where I left it. That was the entire point.
The backup system runs every night, encrypting secrets with age, scanning every diff for leaks, pushing to a private repo. If the whole box gets wiped tomorrow, I can run devenv-bootstrap.sh on a fresh box and be back online in forty minutes.
I keep thinking about how much of this Claude did. Not in a "wow AI is amazing" way. More in a "I used to spend weekends on this stuff" way. The SSH config, the tunnel manager, the provisioning script, the backup system, the leak scanner, the service files, the Notion documentation. I told it what I wanted and it built the thing, caught its own mistakes, and documented the whole process.
Domenic's post gave me the map. Claude Code drove the whole way. I sat in the passenger seat, occasionally shouting rename instructions and watching it come together.
The agents are running now. brave-otter has been working on something for an hour. I should probably go check on it. Or I could just message Slack and ask for a summary.
Yeah. That one.