Running Coding Agents Remotely: SSH, tmux, and the Quest for a Seamless Experience
,Why This Matters: Coding Agents Are Changing How I Work
Coding agents like Claude Code are quite powerful, but watching them work can be like watching paint dry—especially when you're just clicking "approve" repeatedly. Running them with --dangerously-skip-permissions
speeds things up, but doing that on your main development machine? That's asking for trouble.
My solution: dedicate an old Mac mini as a sacrificial lamb for agent experiments. But this created new challenges that took me down a rabbit hole of SSH, tmux, and shell configuration.
The Requirements
For a truly productive remote agent setup, I needed:
- Copy and paste functionality - Essential for grabbing output, errors, or interesting code snippets
- SSH agent forwarding - So agents can push their work to dedicated branches for later review
- Multiple parallel sessions - Because why run one agent when you can run five?
What seemed simple turned into a journey through the quirks of terminal multiplexers and shell environments.
The Setup: tmux as the Foundation
tmux is perfect for this use case—it keeps sessions alive when you disconnect and lets you manage multiple agent runs. But getting it to play nicely with modern requirements took some work.
Making Copy and Paste Work
The first challenge was copy and paste. On macOS, tmux doesn't automatically integrate with the system clipboard. The solution ended up being just three lines added to my .tmux.conf
:
# The important part for clipboard integration
set-option -g mode-keys vi # has to be before bind-key -T...
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"
bind-key -T copy-mode y send-keys -X copy-pipe-and-cancel "pbcopy"
Now you can:
- Press
Ctrl-b [
to enter copy mode - Navigate with vi keybindings
- Press
Space
to start selection - Press
y
to copy to system clipboard
It's not as smooth as native terminal selection, but it works reliably.
The SSH Agent Forwarding Challenge
This was the trickier problem. When you SSH with agent forwarding (ssh -A
), your connection gets a temporary socket like /tmp/ssh-ABC/agent.123
. But when you attach to an existing tmux session, it still has the old socket path from when it was created. Your SSH keys? Useless.
Understanding the Problem
Here's what happens:
- You SSH to your remote Mac with agent forwarding
- SSH creates a new agent socket
- You attach to an existing tmux session
- That session has the old
SSH_AUTH_SOCK
value - Git operations fail because the old socket is gone
The Solution: A Two-Part Approach
Part 1: Smart Session Attachment
First, I vibe-coded together a ta
(tmux attach) function for my local machine. It's ugly and long, but whatever—it works:
# ~/.config/fish/functions/ta.fish
# ta - "tmux attach" - Connect to remote tmux sessions with SSH agent forwarding
#
# This function provides an interactive way to connect to tmux sessions on remote hosts
# with proper SSH agent forwarding support. It uses fzf for session selection.
#
# How it works:
# 1. Takes a remote host as argument (can be user@host or an SSH config alias)
# 2. Connects via SSH to list available tmux sessions
# 3. Presents sessions in an interactive fzf menu with preview
# 4. Connects to the selected session with SSH agent forwarding (-A flag)
#
# SSH Agent Forwarding:
# - Uses 'ssh -A' to forward your local SSH agent to the remote host
# - This allows you to use your local SSH keys within the remote tmux session
# - Requires refresh_tmux_vars function on the remote host to update SSH_AUTH_SOCK
# in existing shells (new shells/panes will work automatically)
#
# Special features:
# - Ctrl-N in the session list to create a new session
# - Automatically offers to create a session if none exist
# - Shows session details (windows, creation time) in the preview pane
#
# Requirements:
# - fzf installed locally for the interactive selection
# - tmux installed on the remote host
# - SSH access to the remote host
# - (Optional) refresh_tmux_vars function on remote for existing shells
#
# Usage:
# ta myserver # Connect using SSH config alias
# ta user@host.com # Connect using full hostname
#
function ta
# Check if host argument is provided
if test (count $argv) -eq 0
echo "Usage: ta <user@host|ssh-alias>"
echo "Example: ta user@example.com"
echo "Example: ta myserver"
return 1
end
set -l remote_host $argv[1]
# Check if fzf is installed
if not command -v fzf >/dev/null
echo "Error: fzf is not installed. Please install it first."
echo " brew install fzf # macOS"
echo " sudo apt install fzf # Ubuntu/Debian"
return 1
end
# Check SSH connection and tmux availability
echo "Checking connection to $remote_host..."
if not ssh -o ConnectTimeout=5 -o BatchMode=yes $remote_host "command -v tmux" >/dev/null 2>&1
echo "Error: Cannot connect to $remote_host or tmux is not installed"
return 1
end
# Get list of tmux sessions
set -l sessions (ssh -A $remote_host "tmux list-sessions 2>/dev/null" | string collect)
if test -z "$sessions"
echo "No tmux sessions found on $remote_host"
read -P "Create a new session? [Y/n] " -n 1 create_new
echo
if test "$create_new" != "n" -a "$create_new" != "N"
read -P "Session name (leave empty for default): " session_name
if test -n "$session_name"
ssh -A -t $remote_host "tmux new-session -s '$session_name'"
else
ssh -A -t $remote_host "tmux new-session"
end
end
return
end
# Use fzf to select a session
# Pass the raw session list to fzf, let it parse naturally
set -l selected (echo "$sessions" | \
fzf --height=50% \
--header="Select tmux session on $remote_host (Ctrl-N for new)" \
--preview='echo "Session info:"; echo {}' \
--preview-window=right:50% \
--bind='ctrl-n:execute(echo ":::NEW_SESSION:::")+abort')
# Handle fzf exit/cancellation
if test -z "$selected"
echo "No session selected"
return
end
# Check if user wants a new session
if test "$selected" = ":::NEW_SESSION:::"
read -P "New session name: " new_session_name
if test -n "$new_session_name"
ssh -A -t $remote_host "tmux new-session -s '$new_session_name'"
else
ssh -A -t $remote_host "tmux new-session"
end
return
end
# Extract session name from selection (everything before the first colon)
set -l session_name (string split -m 1 ':' $selected | head -1)
# Connect to the selected session
echo "Connecting to session: $session_name"
ssh -A -t $remote_host "tmux attach-session -t '$session_name'"
end
This function:
- Connects with agent forwarding enabled
- Shows all available sessions in an interactive menu
- Attaches to your chosen session
Part 2: Automatic Environment Updates
The real magic happens on the remote machine. I added this to my Fish shell configuration:
# ~/.config/fish/config.fish on the remote Mac
# ssh agent forwarding
if status is-interactive
# Only run in tmux sessions
if set -q TMUX
# Function to update SSH agent socket from tmux environment
function __update_ssh_auth_sock
set -l sock (tmux showenv SSH_AUTH_SOCK 2>/dev/null | string split = | tail -1)
if test -n "$sock" -a "$sock" != "-SSH_AUTH_SOCK" -a "$sock" != "$SSH_AUTH_SOCK"
set -gx SSH_AUTH_SOCK $sock
end
end
# Update immediately when shell starts (fixes new panes/windows)
__update_ssh_auth_sock
# Update before each command (fixes existing shells after reattach)
function __ssh_agent_preexec --on-event fish_preexec
__update_ssh_auth_sock
end
# Alternative: Update on each prompt (more reliable but slightly less efficient)
# Uncomment this if the preexec version doesn't work reliably
# function __ssh_agent_prompt --on-event fish_prompt
# __update_ssh_auth_sock
# end
end
end
And the corresponding function:
# ~/.config/fish/functions/refresh_tmux_vars.fish
function refresh_tmux_vars --on-event fish_preexec
if set -q TMUX
# Parse tmux showenv output and set variables
tmux showenv -s | while read -l line
# Match lines like: SSH_AUTH_SOCK="/tmp/ssh-xxx/agent.xxx"; export SSH_AUTH_SOCK;
if string match -qr '^(SSH_AUTH_SOCK|DISPLAY)=(.+); export' -- $line
# Extract variable name and value
set -l var (string match -r '^([^=]+)' -- $line)[2]
set -l val (string match -r '="([^"]+)"' -- $line)[2]
# Set the variable if we found both
if test -n "$var" -a -n "$val"
set -gx $var $val
end
end
end
end
end
This setup:
- Checks tmux's environment for the current SSH_AUTH_SOCK
- Updates the shell's environment to match
- Runs automatically before every command
The key insight: work with tmux's environment preservation, not against it.
Why This Architecture?
You might wonder: why not just use Docker? Or why not run agents locally?
Docker limitations:
- Restricted access to system resources
- Complex volume mounting for projects
- Performance overhead
- Less flexibility for agent experimentation
Remote physical machine advantages:
- Full system access for agents
- Easy to wipe and restart if things go wrong
- Can run multiple resource-intensive agents
- Keeps your main machine safe from experiments
What Didn't Work: The mosh Experiment
Before settling on this SSH + tmux setup, I also tried using mosh (mobile shell) for its better connection persistence and responsiveness. Unfortunately, I hit a showstopper: connecting from an M-series Mac to an Intel-based Mac mini causes the mosh client to segfault. So much for that idea.
Update: I figured out what was wrong with mosh! It wasn't actually an M-series to Intel architecture issue. A Homebrew update had switched mosh's dependency from the older versioned `protobuf@29` to the unversioned `protobuf`, causing a library mismatch that led to segfaults.
The fix was simple:
brew uninstall --ignore-dependencies protobuf protobuf@29
brew reinstall protobuf
brew reinstall mosh
What I Might Try Next
One promising alternative I'm considering is VibeTunnel. It looks like it could provide some of the benefits I was hoping to get from mosh without the architecture compatibility issues. If anyone has experience with it for this use case, I'd love to hear about it.
Conclusion
Running coding agents on a dedicated remote machine solves the safety problem of --dangerously-skip-permissions
while enabling parallel experimentation. The setup requires some initial configuration work, but the payoff is worth it: you can run multiple agents simultaneously, safely experiment with permissions, and maintain full system access for complex tasks.
Is it perfect? No. But it's a working solution that beats watching a single agent slowly modify files on your main machine.