From fa6a8784872cdb7e5f8292145c0e47353868f214 Mon Sep 17 00:00:00 2001 From: David Tomaschik Date: Sat, 18 Apr 2026 19:20:43 -0700 Subject: [PATCH] Fix SSH agent forwarding clobbered by local agent in shenv (#14) * Fix SSH agent forwarding clobbered by local agent in shenv ssh/rc saves the raw forwarded socket in SSH_REMOTE_AUTH_SOCK before rewriting SSH_AUTH_SOCK to the stable symlink. shenv was ignoring that variable, so it saw SSH_AUTH_SOCK as "our link" and fell through to the systemd lookup, which could overwrite the symlink with a local agent socket and silently drop the forwarded one. Now shenv checks SSH_REMOTE_AUTH_SOCK first, giving forwarded sockets priority over any local agent. https://claude.ai/code/session_01RhXaFzxJA5D2BcGcz18ipA * Fix shenv clobbering forwarded SSH socket with local agent in tmux ssh/rc env changes (including SSH_REMOTE_AUTH_SOCK) are lost because ssh/rc runs as a sshd child process, not the user's shell. The shell always receives SSH_AUTH_SOCK set to the raw forwarded socket path. Fresh SSH login worked fine (step 1 catches the raw socket). The bug was in tmux new windows: SSH_AUTH_SOCK there is our stable symlink, so step 1 fails, then steps 2/3 look up the system agent and overwrite the symlink that ssh/rc just set to the forwarded socket. Fix: only run the system agent lookup when the stable symlink is already broken. A valid symlink means ssh/rc (or a previous shenv run) already set it correctly; don't clobber it. https://claude.ai/code/session_01RhXaFzxJA5D2BcGcz18ipA * Remove pointless exports from ssh/rc, add process-model comment ssh/rc runs as a sshd child process so exports never reach the user's shell. SSH_REMOTE_AUTH_SOCK was set and exported but never used (a leftover from a prior failed fix attempt). SSH_AUTH_SOCK was reassigned to the symlink path and exported, also to no effect. Remove both. https://claude.ai/code/session_01RhXaFzxJA5D2BcGcz18ipA --------- Co-authored-by: Claude --- dotfiles/shenv | 14 +++++++++----- dotfiles/ssh/rc | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dotfiles/shenv b/dotfiles/shenv index cdb0382..368dfd3 100755 --- a/dotfiles/shenv +++ b/dotfiles/shenv @@ -113,13 +113,17 @@ _is_link_path() { _CANDIDATE="" -# 1. If current environment has a valid socket that is NOT our link, it's a prime candidate (e.g. SSH forwarding). +# 1. If current environment has a valid socket that is NOT our link, it's a prime candidate +# (e.g. fresh SSH login: sshd sets SSH_AUTH_SOCK to the raw forwarded socket before ssh/rc +# rewrites it to the stable symlink; the shell inherits the original raw path). if [ -S "${SSH_AUTH_SOCK:-}" ] && ! _is_link_path "${SSH_AUTH_SOCK}"; then _CANDIDATE="${SSH_AUTH_SOCK}" fi -# 2. If no candidate yet, or we're currently using the link, try to find the "real" system agent. -if [ -z "${_CANDIDATE}" ] || _is_link_path "${SSH_AUTH_SOCK:-}"; then +# 2. Only look for a system agent if the stable link is already broken. If the link is +# valid (e.g. a tmux pane where SSH_AUTH_SOCK points to our symlink which ssh/rc just +# updated to the forwarded socket), leave it alone — don't clobber it with a local agent. +if [ -z "${_CANDIDATE}" ] && [ ! -S "${_SSH_AUTH_LINK}" ]; then _FOUND="" if [ "$(uname)" = "Darwin" ]; then _FOUND=$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null) @@ -143,8 +147,8 @@ if [ -z "${_CANDIDATE}" ] || _is_link_path "${SSH_AUTH_SOCK:-}"; then fi fi -# 3. Last resort: search common paths if we still don't have a valid candidate. -if [ ! -S "${_CANDIDATE}" ]; then +# 3. Last resort: search common paths if we still don't have a candidate and the link is broken. +if [ ! -S "${_CANDIDATE}" ] && [ ! -S "${_SSH_AUTH_LINK}" ]; then _U=$(id -u) for _p in "/run/user/${_U}/keyring/ssh" "/run/user/${_U}/ssh-agent.socket" "/run/user/${_U}/openssh_agent" "/run/user/${_U}/gnupg/S.gpg-agent.ssh"; do if [ -S "${_p}" ] && ! _is_link_path "${_p}"; then diff --git a/dotfiles/ssh/rc b/dotfiles/ssh/rc index 8745f16..70dff92 100755 --- a/dotfiles/ssh/rc +++ b/dotfiles/ssh/rc @@ -2,19 +2,19 @@ # Roughly based on this article: # https://werat.github.io/2017/02/04/tmux-ssh-agent-forwarding.html +# +# NOTE: this file is executed by sshd as a child process, NOT sourced by the +# user's shell. Any variable assignments or exports here have no effect on the +# shell environment the user will land in. REMOTE_LINK="${HOME}/.ssh/ssh_auth_sock" if [ -S "${SSH_AUTH_SOCK}" ] ; then - SSH_REMOTE_AUTH_SOCK="${SSH_AUTH_SOCK}" - export SSH_REMOTE_AUTH_SOCK # Always update the symlink to the latest session's socket. # This ensures that tmux (which uses the static path) always points to a # current agent. mkdir -p "$(dirname "${REMOTE_LINK}")" ln -sf "${SSH_AUTH_SOCK}" "${REMOTE_LINK}" - SSH_AUTH_SOCK="${REMOTE_LINK}" - export SSH_AUTH_SOCK fi # if stdin is a tty, don't do the cookie step