/blog/

2026 0314 SSH, tmux, and agent forwarding

By default, using the same tmux session from multiple ssh sessions breaks agent forwarding.

Here’s why: When tmux starts, it inherits the environment. By default, that environment is fixed. ssh sets some environment variables, like SSH_CONNECTION and, if you forwarded an agent, SSH_AUTH_SOCK, but those will only work properly on the ssh connection that started the tmux session. Connecting to the same tmux session later over a new ssh connection will use the values from the original ssh connection, so they’ll be out of date. Trying to use the same session from both a local terminal and an ssh connection will have the same problem.

Furthermore, if we are using ssh to connect out from tmux (for instance, pulling git repos inside of the tmux session), we’d like to use a local agent if our session is local1, but a forwarded agent if one is available from an ssh connection.

To fix these issues, we need to do two things:

  1. Configure the shell to update tmux’s environment on every prompt
  2. Configure ssh to only use a local agent from a non-ssh connection (which we have already done previously)

My configuration

Here’s what I do in my .zshrc to configure zsh:

tmux_update_environment() {
    test -n "$TMUX" && test -z "$TMUX_NO_UPDATE_ENVIRONMENT" || return
    eval $(tmux show-environment -s)
}
preexec_functions+=(tmux_update_environment)

And here’s how I set ~/.ssh/config to use the 1Password agent only if it’s not being run over an ssh connection:

Host *
  Match exec "test -z $SSH_CONNECTION"
    IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

Adapt this to your shell and preferred local agent, and

Technical details

This works well for me. It’s worth going into some technical detail here as there are some tradeoffs and you may prefer to make some different ones.

tmux environment

I pull in everything that tmux show-environment -s returns. For me right now on tmux 3.5a on macOS, the result of that command is:

unset DISPLAY;
unset KRB5CCNAME;
unset SSH_AGENT_PID;
unset SSH_ASKPASS;
SSH_AUTH_SOCK="/private/tmp/com.apple.launchd.gxn88S3JmK/Listeners"; export SSH_AUTH_SOCK;
unset SSH_CONNECTION;
unset WINDOWID;
unset XAUTHORITY;

This is a curated set of environment vars that tmux tracks for each session. (It’s clearly not the entire environment from when it was started, but you can see that with tmux show-environment -g if you want.) All of them may change as you connect to tmux sessions from different environments.

Setting them in the zsh preexec_functions list means that these will get overwritten after hitting enter but before the command is run. Setting any of those variables on the command line will get overwritten immediately with no notice. This might be confusing!

That said, you can work around it as long as you remember to do so. I add a check for TMUX_NO_UPDATE_ENVIRONMENT so that I can disable this behavior in a given shell session if necessary.

If you want this behavior on only some of these, you can always filter the list.

zsh hook functions

You could set this in precmd_functions instead. As the docs say, precmd is executed before each prompt; preexec is executed just after a command has been read from the user, but before it is executed.

SSH configuration

The SSH configuration is a bit tricky. As I discovered last time, it’s impossible to quote that SSH_CONNECTION variable correctly, but it works just fine if you don’t quote it at all. It can do Match lines like that, but it doesn’t do environment or tilde expansion in the IdentityAgent setting, so you must hard-code the absolute path.

Also, previously I used SSH_TTY, which works fine for interactive sessions, but SSH_CONNECTION is set even for noninteractive sessions, and using it instead lets this work properly when doing something like

ssh me@mac-mini.local sh -c 'cd ~/some-repo; git pull;'

  1. Agents like 1Password or those that require using TouchID or a YubiKey will just hang if you attempt to use them over an SSH connection, which is not a great experience. ↩︎

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).