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. 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