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

Craig Younkins
2 min readJun 26, 2021

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. In ~/.bash_aliases:

alias refresh-tmux=”eval \$(tmux showenv -s | grep -E ‘^(SSH|DISPLAY)’)

Note that the $ is escaped so it’s executed at the right time.

That’s great, 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 update the variables 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

--

--

Craig Younkins

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