Fixing Broken SSH / X11 Forwarding with tmux (and fish!)

When you resume a tmux session on a remote server, tmux aims to restore the environment exactly as it was when you left. That’s great, except when you need things to change.

The SSH_AUTH_SOCK and DISPLAY environment variables are set by SSH when the connection is established, and are inherited by your shell upon initial creation. When resuming a session, the underlying SSH connection has updated environment variables, but your shell doesn’t get them because tmux dutifully preserved the environment.

So when you get

Permission denied (publickey).
fatal: Could not read from remote repository.

or (for DISPLAY issue):

cannot open display

but the same command works when you open a new window or tab, it’s probably because the environment variables are stale.

So how do we fix this? For SSH, some folks believe that the socket shouldn’t really move and so they make a symlink at a static path and update the symlink when possible. Unfortunately I don’t think that works for X11 forwarding.

Another option is to ask tmux for the current environment variables and pull them into our shell. tmux provides tmux showenv to retrieve the current environment variables and will even format them for Bash consumption. To refresh SSH and DISPLAY variables in Bash:

eval $(tmux showenv -s | grep -E '^(SSH|DISPLAY)')

For fish shell it requires some regex gymnastics. This is what I came up with:

tmux showenv -s | string replace -rf '^((?:SSH|DISPLAY).*?)=(".*?"); export.*' 'set -gx $1 $2' | source

You can put that in a function and call it each time something is broken, but I like the idea of never having to think about it. The best way I’ve found to have it be automatically fixed is to run the above before every interactive command, called from a hook called preexec.

Unfortunately traditional Bash doesn’t seem to have a preexec hook (could try bash-preexec), but zsh supports it natively. In .zshrc on the remote server:

function refresh_tmux_vars {
if [ -n "$TMUX" ]; then
eval $(tmux showenv -s | grep -E '^(SSH|DISPLAY)')
fi
}
function preexec {
refresh_tmux_vars
}

And here’s the same for fish in ~/.config/fish/functions/refresh_tmux_vars.fish on the remote server:

function refresh_tmux_vars --on-event="fish_preexec"
if set -q TMUX
tmux showenv -s | string replace -rf '^((?:SSH|DISPLAY).*?)=(".*?"); export.*' 'set -gx $1 $2' | source
end
end

Happy tmuxing!

— — — —

Additional reading:

Renew environment variables in tmux

Reconciling Tmux and SSH Agent Forwarding

SSH agent forwarding and screen

How to auto-update SSH agent environment variables when attaching to existing tmux sessions

Pro-Tip — SSH_AUTH_SOCK, tmux and you

Hacker, entrepreneur, and quantified self nerd. cyounkins at gmail.

Hacker, entrepreneur, and quantified self nerd. cyounkins at gmail.

Share your ideas with millions of readers.

I’m recently single, and being a nerd I brought my OKCupid profile out of its dormant state to hopefully meet likeminded women. I looked at my old photos and thought, “I should take a new one.”

But what kind of photo? I decided to put it to the test and…

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store