Summary
On WSL, the ssh-agent relay that bridges to the Windows agent is started only from dot_profile. Any shell that does not source ~/.profile — non-login and non-interactive shells — runs without SSH_AUTH_SOCK and without the socat listener. SSH then fails with an error that points at keys rather than at the missing bridge, which costs a lot of time to diagnose.
Environment-specific detail (key inventory, absolute paths, incident log) is tracked in a private companion issue.
The chain
WSL ssh -> ~/.ssh/agent.sock (socat UNIX-LISTEN)
-> npiperelay.exe -ei -s //./pipe/openssh-ssh-agent
-> Windows ssh-agent pipe
Root cause
dot_profile lines 26-75 gate the whole relay behind [ -n "$WSL_DISTRO_NAME" ], and lines 59-63 spawn the listener:
if ! ss -a 2>/dev/null | grep -Fq "$SSH_AUTH_SOCK"; then
rm -f "$SSH_AUTH_SOCK"
(setsid socat UNIX-LISTEN:"$SSH_AUTH_SOCK",fork EXEC:"$npiperelay_path -ei -s //./pipe/openssh-ssh-agent" >/dev/null 2>&1 &)
fi
~/.profile is sourced by login shells only. Shells spawned by coding agents, wsl -e, cron, systemd user services, and some IDE terminals are neither login nor interactive, so:
SSH_AUTH_SOCK is unset
- no
socat process is started
- nothing repairs the relay if it died earlier in the boot
The listener is also session-bound: when the process that started it goes away, the relay goes with it, and nothing brings it back until the next login shell.
Why the failure is hard to diagnose
A dead relay leaves the socket file at ~/.ssh/agent.sock behind. It still stats as a socket, so it looks healthy. Connecting to it yields Connection refused, ssh falls through to on-disk key auth, which cannot complete non-interactively, and reports:
ssh_askpass: exec(/usr/bin/ssh-askpass): No such file or directory
git@github.com: Permission denied (publickey).
Nothing in that output mentions the relay, socat, or npiperelay. The message implicates the key or the remote's authorized keys, so investigation starts in the wrong place. It is also indistinguishable from "the user has not completed approval on the Windows side," which sends you off waiting on a prompt that was never going to arrive.
One further red herring: dot_profile line 77 falls back to the ~/.ssh/agent.env ssh-agent path when WSL_SSH_RELAY_ACTIVE is unset. That file persists with a stale SSH_AUTH_SOCK and a dead PID, so sourcing it looks like progress and yields another dead socket.
Repro
env -i /bin/bash -c 'ssh -T git@github.com' # no relay, no SSH_AUTH_SOCK
Or: let the relay die, then run any non-login shell and attempt an SSH push.
Proposed fix
Either or both:
1. Make the relay session-independent. Move it out of dot_profile into a systemd user unit (WSL2 supports systemd) with a socket-activated or always-on listener, so it survives across sessions and does not depend on how the shell was spawned. Alternatively, extract an idempotent ensure-ssh-relay script that both dot_profile and dot_bashrc call, and that reaps a stale socket before binding.
2. Make the failure self-describing. Even with (1), the diagnostic gap is worth closing:
- Have the relay-start path detect a stale-but-present
~/.ssh/agent.sock (exists, not listening per ss) and log a specific message naming the relay.
- Provide a
just recipe or script — e.g. ssh-relay-status — that reports npiperelay path, socat presence, whether the socket is bound, and ssh-add -l output.
- Consider removing or refreshing
~/.ssh/agent.env when the WSL relay path is the active one, so the fallback does not resurrect a dead agent.
Discoverability for agents
AGENTS.md documents GitHub CLI usage, jj co-location, and live-systems safety, but says nothing about the SSH relay. An agent that hits Permission denied (publickey) has no pointer toward the bridge and will reasonably conclude the key or the auth prompt is at fault.
Suggest adding a short section to AGENTS.md, roughly:
ssh auth (WSL)
SSH to GitHub/Codeberg goes through a relay: ~/.ssh/agent.sock (socat) ->
npiperelay.exe -> Windows ssh-agent pipe. The relay is started by
~/.profile and therefore is not running in non-login shells.
Permission denied (publickey) in an agent shell usually means the relay is
down, not that the key is wrong. Check ss -a | grep agent.sock and
ps -ef | grep socat before touching keys or ssh config. Note that
~/.ssh/agent.sock may exist as a stale file while nothing is listening.
Restart with <ensure-ssh-relay recipe>; do not assume the operator needs to
re-authenticate.
Also worth noting there: dot_ssh/config.tmpl defines github.com / codeberg.org blocks with IdentityAgent //./pipe/openssh-ssh-agent, but the deployed WSL ~/.ssh/config intentionally omits them (that path is a Windows named pipe WSL's ssh cannot open). chezmoi status does not flag this, so it reads as drift when it is not.
Acceptance criteria
Summary
On WSL, the ssh-agent relay that bridges to the Windows agent is started only from
dot_profile. Any shell that does not source~/.profile— non-login and non-interactive shells — runs withoutSSH_AUTH_SOCKand without thesocatlistener. SSH then fails with an error that points at keys rather than at the missing bridge, which costs a lot of time to diagnose.The chain
Root cause
dot_profilelines 26-75 gate the whole relay behind[ -n "$WSL_DISTRO_NAME" ], and lines 59-63 spawn the listener:~/.profileis sourced by login shells only. Shells spawned by coding agents,wsl -e, cron, systemd user services, and some IDE terminals are neither login nor interactive, so:SSH_AUTH_SOCKis unsetsocatprocess is startedThe listener is also session-bound: when the process that started it goes away, the relay goes with it, and nothing brings it back until the next login shell.
Why the failure is hard to diagnose
A dead relay leaves the socket file at
~/.ssh/agent.sockbehind. It still stats as a socket, so it looks healthy. Connecting to it yieldsConnection refused, ssh falls through to on-disk key auth, which cannot complete non-interactively, and reports:Nothing in that output mentions the relay,
socat, or npiperelay. The message implicates the key or the remote's authorized keys, so investigation starts in the wrong place. It is also indistinguishable from "the user has not completed approval on the Windows side," which sends you off waiting on a prompt that was never going to arrive.One further red herring:
dot_profileline 77 falls back to the~/.ssh/agent.envssh-agent path whenWSL_SSH_RELAY_ACTIVEis unset. That file persists with a staleSSH_AUTH_SOCKand a dead PID, so sourcing it looks like progress and yields another dead socket.Repro
Or: let the relay die, then run any non-login shell and attempt an SSH push.
Proposed fix
Either or both:
1. Make the relay session-independent. Move it out of
dot_profileinto a systemd user unit (WSL2 supports systemd) with a socket-activated or always-on listener, so it survives across sessions and does not depend on how the shell was spawned. Alternatively, extract an idempotentensure-ssh-relayscript that bothdot_profileanddot_bashrccall, and that reaps a stale socket before binding.2. Make the failure self-describing. Even with (1), the diagnostic gap is worth closing:
~/.ssh/agent.sock(exists, not listening perss) and log a specific message naming the relay.justrecipe or script — e.g.ssh-relay-status— that reports npiperelay path,socatpresence, whether the socket is bound, andssh-add -loutput.~/.ssh/agent.envwhen the WSL relay path is the active one, so the fallback does not resurrect a dead agent.Discoverability for agents
AGENTS.mddocuments GitHub CLI usage, jj co-location, and live-systems safety, but says nothing about the SSH relay. An agent that hitsPermission denied (publickey)has no pointer toward the bridge and will reasonably conclude the key or the auth prompt is at fault.Suggest adding a short section to
AGENTS.md, roughly:Also worth noting there:
dot_ssh/config.tmpldefinesgithub.com/codeberg.orgblocks withIdentityAgent //./pipe/openssh-ssh-agent, but the deployed WSL~/.ssh/configintentionally omits them (that path is a Windows named pipe WSL's ssh cannot open).chezmoi statusdoes not flag this, so it reads as drift when it is not.Acceptance criteria
ssh -T git@github.comsuccessfully without first opening a login shell.~/.ssh/agent.sockis detected and replaced rather than causingConnection refused.AGENTS.mdpoints at the relay as the first thing to check forPermission denied (publickey)on WSL.