Skip to content

WSL ssh-agent relay starts only in login shells; failure misdiagnoses as a key problem #129

Description

@aguil

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

  • A non-login, non-interactive shell can ssh -T git@github.com successfully without first opening a login shell.
  • A stale ~/.ssh/agent.sock is detected and replaced rather than causing Connection refused.
  • A single documented command reports relay health.
  • AGENTS.md points at the relay as the first thing to check for Permission denied (publickey) on WSL.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions