My Dev Environment in the Vibe Coding Era: Reachable, Not Just Fast 🛰️
A few months ago I wrote about the keyboard-first setup that removed the mouse from most of my day.
That setup is still here. But the shape of my work has changed underneath it.
I type less code than I used to. I spend more time describing intent, reviewing diffs, approving (or rejecting) plans, and nudging an agent back on track. A lot of the “work” now happens while I’m not looking: a long refactor, a test run, a migration being drafted.
That changes what I want from a dev environment.
It used to be about speed at the desk. Now it’s mostly about reachability from anywhere.
If an agent is halfway through a task and needs one decision from me, I don’t want that to wait until I’m back at my desk. I want to answer it from my phone, in a queue, in about 30 seconds, and go back to my day.
This article is the stack that makes that possible:
- Tailscale: one private network for all my devices
- SSH + tmux: the terminal is still the main interface, from any device
- Claude Code Remote Control: a nicer UI for steering sessions from my phone
- AnyDesk / TeamViewer: the escape hatch when something needs a GUI
- a few small dotfile tweaks that make the whole thing feel seamless
The philosophy: the machine works, I check in
Vibe coding isn’t (for me) “let the AI write everything and hope.” It’s closer to managing a very fast, very literal junior engineer who never gets tired.
Agents are good at grinding. They’re bad at knowing when they’re wrong.
So the bottleneck moves. It’s no longer how fast I can type. It’s how quickly I can:
- see what an agent is doing
- unblock it when it’s waiting on me
- stop it when it’s going the wrong way
All three are latency problems. And latency is mostly an access problem.
My Mac stays home, plugged in, always on. Every other device I own is just a window into it.
Tailscale: the network I stopped thinking about
Tailscale (tailscale.com) is the foundation. It puts my Mac, my servers, and my phone on one private network (a “tailnet”) using WireGuard under the hood.
What it removed:
- no port forwarding on the home router
- no public SSH port getting hammered by bots
- no dynamic DNS
- no “which IP is it today?”
Every device gets a stable name. From anywhere (home, office, mobile data, hotel Wi-Fi), ssh ryeng-home just works.
The best infrastructure is the kind you forget exists. I installed Tailscale once, and I haven’t thought about networking since. That’s the highest praise I can give a tool.
SSH + tmux: the terminal is still the center
I still live in the terminal. The vibe coding era didn’t change that; if anything, it made it stronger. Agents like Claude Code are terminal-native, so a terminal is all I need to reach them.
On the phone side I use Termius (termius.com) to SSH into the Mac over Tailscale.
One tmux session per device
I don’t only reach the home Mac from my phone anymore. These days it’s at least three devices: Kitty on the Mac itself, Termius on my phone, and my work MacBook. More will come.
The trick that made this pleasant: every device gets its own tmux session.
main: Kitty on the home Mac auto-attaches heremobile: SSH from my phone attaches here, and it starts in~/projectswith Claude already running- one session per extra Mac: SSH from another Mac gets a session named after that device, so
ryeng-worklands inwork
The reason is simple. If two devices attach to the same session, they fight over the window size. The phone shrinks everything to a tiny width, and when I get back to my desk the layout is a mess. One session per device means no device steals another’s screen, and each one picks up exactly where it left off.
How does the Mac know who’s connecting? Tailscale again. $SSH_CONNECTION holds the client’s tailnet IP, and tailscale whois turns that IP into a device name. There’s no list of devices to keep in sync: a new laptop that joins the tailnet gets its own session the first time it connects.
The relevant bit of my .zshrc:
tmux_ensure_mobile() {
tmux has-session -t mobile 2>/dev/null && return 0
tmux new-session -d -s mobile -c "$HOME/projects"
tmux send-keys -t mobile 'claude' Enter
}
# Tailscale device name of the SSH client (e.g. ryeng-phone, ryeng-work)
ssh_peer_name() {
command_exists tailscale || return 0
tailscale whois "${SSH_CONNECTION%% *}" 2>/dev/null \
| awk '/^ Name:/ { split($2, parts, "."); print parts[1]; exit }'
}
if [[ -z "$TMUX" ]] && command_exists tmux; then
if [[ -n "$SSH_CONNECTION" ]]; then
ssh_peer="$(ssh_peer_name)"
if [[ -n "$ssh_peer" && "$ssh_peer" != *phone* ]]; then
# Another Mac -> its own session (ryeng-work -> "work")
tmux new-session -A -s "${ssh_peer#ryeng-}" -c "$HOME/projects" && exit
else
# Phone, or a client Tailscale doesn't know -> mobile
tmux_ensure_mobile && tmux attach-session -t mobile && exit
fi
unset ssh_peer
elif [[ -n "$KITTY_WINDOW_ID" ]]; then
tmux_ensure_mobile
tmux new-session -A -s main
fi
fi
A few small details I care about:
claudeis typed into the shell, not used as the session command. If I quit Claude, I land in a normal shell instead of a dead session.- Unknown clients fall back to
mobile. If the connection doesn’t come through Tailscale (say, a plain LAN IP),whoisreturns nothing and I still get a sensible session. && exit. Detaching from tmux also ends the SSH login, so I never end up in a stray shell outside tmux. But if tmux itself fails, theexitis skipped and I get a plain shell instead of being kicked out. That matters a lot when SSH is my only way in.
So from my phone: open Termius, tap the host, and I’m already looking at a Claude prompt in my projects folder. From the work Mac: ssh ryeng-home, and I’m back in the work session exactly as I left it. No setup, no tmux attach, no cd.
tmux is also the reconnect story
Mobile connections drop. You switch from Wi-Fi to 4G, the app gets backgrounded, the train goes into a tunnel.
With tmux, that doesn’t matter. The session (and whatever the agent is doing inside it) keeps running on the Mac. Reconnect, and I’m exactly where I left off.
Small things that make SSH feel local
A few tweaks that remove the “I’m on a remote machine” feeling:
- SSH multiplexing.
ControlMaster auto+ControlPersist 600in~/.ssh/config, so the second, third, and tenth connection to the same host are instant. - Clipboard over SSH. Neovim detects when it runs over SSH and switches its clipboard to OSC 52, so yanking text on the remote machine lands on the clipboard of whatever device I’m holding.
- Tunnels on demand. My
kubectlis a small shell wrapper that opens an SSH tunnel to the k3s API on my server if one isn’t already running. I never think about it;kubectl get podsjust works.
local is_ssh = vim.env.SSH_CONNECTION ~= nil or vim.env.SSH_TTY ~= nil
if is_ssh then
-- Remote: OSC 52 → local clipboard
vim.g.clipboard = {
name = "OSC 52",
copy = {
["+"] = require("vim.ui.clipboard.osc52").copy("+"),
["*"] = require("vim.ui.clipboard.osc52").copy("*"),
},
paste = {
["+"] = require("vim.ui.clipboard.osc52").paste("+"),
["*"] = require("vim.ui.clipboard.osc52").paste("*"),
},
}
end
None of this is fancy. Each one just removes a small “ugh” moment, and those add up.
Claude Code Remote Control: steering without a terminal
SSH from a phone works, but let’s be honest: a terminal on a 6-inch screen is not a great place to read a 400-line diff.
That’s where Claude Code’s Remote Control comes in (docs). You start a session on your own machine:
claude remote-control --name "jms-go refactor"
…and it shows up in claude.ai/code and the Claude mobile app. The session still runs locally, with my files, my tools, my MCP servers, and my credentials. The phone is just a remote for it.
What I use it for:
- approving or rejecting a plan before the agent starts editing
- answering “should I do A or B?” questions
- reading diffs and summaries in a UI that was actually designed for a phone
- kicking off a follow-up task when the first one finishes
SSH vs Remote Control: when I use which
They overlap, but they’re good at different things:
| Situation | I reach for |
|---|---|
| An agent is waiting on a decision | Remote Control |
| Reading diffs, plans, and summaries | Remote Control |
| I need a real shell (logs, git, restarting services) | SSH + tmux |
| Something outside Claude needs fixing | SSH + tmux |
| A GUI app or a system dialog is in the way | AnyDesk / TeamViewer |
Rule of thumb: Remote Control for steering, SSH for doing.
AnyDesk / TeamViewer: the escape hatch
Not everything lives in a terminal. Sometimes the thing blocking me is:
- a macOS permission dialog (“Terminal would like to control…”)
- a browser login or OAuth flow that has to happen on the Mac itself
- the iOS Simulator, Xcode, or an app I need to actually look at
- a Keychain prompt
- or the worst one: SSH itself is broken, and I need to see the screen to fix it
For those moments I keep a full remote desktop: AnyDesk (anydesk.com) as my main one, with TeamViewer (teamviewer.com) as a backup.
Do I use it often? No. Maybe a few times a month.
That’s exactly the point. An escape hatch isn’t supposed to be comfortable. It’s supposed to exist. Knowing that any problem on the Mac is fixable from my phone is what lets me trust the rest of the setup enough to walk away from my desk.
Also worth saying: remote desktop tools are a big attack surface. Use strong unattended-access passwords, turn on 2FA, and keep the allowed device list short.
The layers, from most to least used
Put together, it’s a stack of fallbacks:
- At the desk: Kitty + tmux
main+ Neovim + Claude in a tmux popup (same as before) - Away, quick decision: Claude Remote Control in the mobile app
- Away, real work: Termius → Tailscale → SSH → tmux
mobile(or, from the work Mac, its ownworksession) - Away, something’s broken or needs a GUI: AnyDesk / TeamViewer
Each layer covers what the one above can’t. And because they all point at the same machine with the same state, switching between them costs almost nothing.
The hidden downsides
This setup has real costs, too:
- An always-on machine. The Mac needs power, a stable connection, and sleep disabled. If the house loses power, the whole thing is gone.
- Security is on you. More ways in means more doors to lock. Tailscale ACLs, SSH keys only, 2FA on everything, and be careful about what permission mode you let agents run with when nobody is watching.
- The “always reachable” trap. Being able to unblock an agent from anywhere quickly turns into feeling like you should. I try to treat the phone as a way to save time, not a reason to never stop working.
And the same trap from my last post still applies: you can spend more time building the setup than using it. Tooling has to earn its keep.
My configs
- Dotfiles (tmux, zsh, Neovim, SSH, and the per-device tmux sessions): github.com/ryenguyen7411/dot-files
- Previous post on the keyboard-first part of the setup: Neovim, QMK, and the Tooling That Eliminated the Mouse for Me
Enjoyed this post?