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 <noreply@anthropic.com>
This commit is contained in:
David Tomaschik
2026-04-18 19:20:43 -07:00
committed by GitHub
parent 1804357162
commit fa6a878487
2 changed files with 13 additions and 9 deletions

View File

@@ -113,13 +113,17 @@ _is_link_path() {
_CANDIDATE="" _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 if [ -S "${SSH_AUTH_SOCK:-}" ] && ! _is_link_path "${SSH_AUTH_SOCK}"; then
_CANDIDATE="${SSH_AUTH_SOCK}" _CANDIDATE="${SSH_AUTH_SOCK}"
fi fi
# 2. If no candidate yet, or we're currently using the link, try to find the "real" system agent. # 2. Only look for a system agent if the stable link is already broken. If the link is
if [ -z "${_CANDIDATE}" ] || _is_link_path "${SSH_AUTH_SOCK:-}"; then # 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="" _FOUND=""
if [ "$(uname)" = "Darwin" ]; then if [ "$(uname)" = "Darwin" ]; then
_FOUND=$(launchctl getenv SSH_AUTH_SOCK 2>/dev/null) _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
fi fi
# 3. Last resort: search common paths if we still don't have a valid candidate. # 3. Last resort: search common paths if we still don't have a candidate and the link is broken.
if [ ! -S "${_CANDIDATE}" ]; then if [ ! -S "${_CANDIDATE}" ] && [ ! -S "${_SSH_AUTH_LINK}" ]; then
_U=$(id -u) _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 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 if [ -S "${_p}" ] && ! _is_link_path "${_p}"; then

View File

@@ -2,19 +2,19 @@
# Roughly based on this article: # Roughly based on this article:
# https://werat.github.io/2017/02/04/tmux-ssh-agent-forwarding.html # 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" REMOTE_LINK="${HOME}/.ssh/ssh_auth_sock"
if [ -S "${SSH_AUTH_SOCK}" ] ; then 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. # Always update the symlink to the latest session's socket.
# This ensures that tmux (which uses the static path) always points to a # This ensures that tmux (which uses the static path) always points to a
# current agent. # current agent.
mkdir -p "$(dirname "${REMOTE_LINK}")" mkdir -p "$(dirname "${REMOTE_LINK}")"
ln -sf "${SSH_AUTH_SOCK}" "${REMOTE_LINK}" ln -sf "${SSH_AUTH_SOCK}" "${REMOTE_LINK}"
SSH_AUTH_SOCK="${REMOTE_LINK}"
export SSH_AUTH_SOCK
fi fi
# if stdin is a tty, don't do the cookie step # if stdin is a tty, don't do the cookie step