{"slug": "bitwarden-ssh-agent-in-wsl2-via-a-systemd-user-service-socat-npiperelay-with-the", "title": "Bitwarden SSH agent in WSL2 via a systemd user service (socat + npiperelay) — with the gotchas that make ssh-add hang", "summary": "A developer created a systemd user service to bridge Bitwarden Desktop's SSH agent from Windows to WSL2 using socat and npiperelay, solving hanging issues with ssh-add. The solution requires the standalone Bitwarden build (not the Microsoft Store version) and disabling Windows' native ssh-agent.", "body_md": "Use the SSH keys stored in your **Windows Bitwarden Desktop** vault from inside\n**WSL2** — `ssh`\n\n, `git`\n\n, `ssh-add`\n\n, etc. — managed by a **systemd user service**\ninstead of a fragile `.zshrc`\n\nsnippet.\n\nThis bridges the Windows OpenSSH-agent named pipe that Bitwarden exposes\n(`\\\\.\\pipe\\openssh-ssh-agent`\n\n) to a Unix socket inside WSL using\n[ npiperelay](https://github.com/albertony/npiperelay) +\n\n`socat`\n\n.It is a hardened version of the excellent walkthrough at\n[https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/](https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/), with fixes for the\nproblems that make `ssh-add -l`\n\nhang.\n\n```\nWindows                                   │ WSL2\n                                          │\nBitwarden Desktop (standalone build)      │\n  └─ \\\\.\\pipe\\openssh-ssh-agent  ◀────────┼──  npiperelay.exe  ◀──  socat  ◀──  ~/.ssh/agent.sock\n       (one pipe instance)                │      (per connection)   (listener)     ▲\n                                          │                                         │\n                                          │                       ssh / git / ssh-add (SSH_AUTH_SOCK)\n```\n\nA systemd **user** service runs one persistent `socat`\n\nlistener. `socat`\n\nforks a\nshort-lived `npiperelay.exe`\n\nfor each incoming connection, which relays bytes to\nBitwarden's named pipe.\n\nThe **Microsoft Store / Appx** build of Bitwarden Desktop runs in a UWP\nAppContainer sandbox and **does not serve the SSH agent to external clients**.\nThe pipe appears and connections *open*, but Bitwarden never replies — so\neverything hangs, including the native Windows `ssh-add.exe`\n\n.\n\nCheck what you have (PowerShell):\n\n```\nGet-AppxPackage *Bitwarden*      # if this prints anything, you have the Store build — uninstall it\n```\n\nFix — remove the Store/Appx build, then install the standalone build:\n\n```\n# Remove the Store (Appx) build. winget can't uninstall it by that name, so use Appx:\nGet-AppxPackage *Bitwarden* | Remove-AppxPackage\n\n# Install the standalone build (validated winget id):\nwinget install Bitwarden.Bitwarden\n```\n\nOr download the desktop installer from [https://bitwarden.com/download/](https://bitwarden.com/download/).\n\nBitwarden → **Settings → SSH agent → Enable SSH agent**. Have at least one SSH\nkey item in your vault, and keep the vault **unlocked** (the agent only answers\nwhile unlocked).\n\nIt would fight Bitwarden for the same pipe name. PowerShell (admin):\n\n```\nStop-Service ssh-agent\nSet-Service ssh-agent -StartupType Disabled\n```\n\n- WSL2 with\n**systemd enabled**—`/etc/wsl.conf`\n\n:(then\n\n```\n[boot]\nsystemd=true\n```\n\n`wsl --shutdown`\n\nfrom Windows once) `socat`\n\ninstalled:`sudo apt install socat`\n\n`npiperelay.exe`\n\non the Windows side. Easiest:then find the path (it lives under\n\n```\nwinget install albertony.npiperelay\n```\n\n`C:\\Users\\<you>\\AppData\\Local\\Microsoft\\WinGet\\Packages\\albertony.npiperelay_*\\npiperelay.exe`\n\n).**Enable lingering** so the service runs without an interactive login:\n\n```\nloginctl enable-linger \"$USER\"\n```\n\n-\n**Find your npiperelay path** and put it in`bw-ssh-relay`\n\n(edit the`NPIPERELAY=`\n\nline). From WSL:\n\n```\nls /mnt/c/Users/*/AppData/Local/Microsoft/WinGet/Packages/albertony.npiperelay_*/npiperelay.exe\n```\n\n-\n**Install the relay script:**\n\n```\nmkdir -p ~/.local/bin\ncp bw-ssh-relay ~/.local/bin/bw-ssh-relay\nchmod +x ~/.local/bin/bw-ssh-relay\n```\n\n-\n**Install the service:**\n\n```\nmkdir -p ~/.config/systemd/user\ncp bitwarden-ssh-agent.service ~/.config/systemd/user/\nsystemctl --user daemon-reload\nsystemctl --user enable --now bitwarden-ssh-agent.service\n```\n\n-\n**Point your shell at the socket.** Add to`~/.bashrc`\n\n/`~/.zshrc`\n\n:\n\n```\nexport SSH_AUTH_SOCK=\"$HOME/.ssh/agent.sock\"\n```\n\n-\n**Test**(open a new shell):\n\n```\nssh-add -l        # should list your Bitwarden SSH key(s)\n```\n\n| File | Goes to | Purpose |\n|---|---|---|\n`bw-ssh-relay` |\n`~/.local/bin/bw-ssh-relay` |\nLaunches the `socat` ↔ `npiperelay` bridge (the service's `ExecStart` ). |\n`bitwarden-ssh-agent.service` |\n`~/.config/systemd/user/` |\nsystemd user unit that keeps one relay listener running and restarts it on failure. |\n\nThis is the upstream-article command: ** -ei -s** — nothing more.\n\n| Flag | Meaning |\n|---|---|\n`-ei` |\nTerminate when the client (stdin) closes — clean teardown per connection. This is what prevents process leaks, so it must be able to fire. |\n`-s` |\nSend a 0-byte message after EOF (part of the original recipe). |\n\n⚠️\n\nDo NOT addIt seems helpful (\"poll until the pipe is free\") but it is a trap with Bitwarden. Bitwarden exposes only`-p`\n\n.onepipe instance; when it is busy or wedged,`-p`\n\nmakes npiperelay loop forever in`DialPipe`\n\n. While polling it never reads stdin, so`-ei`\n\ncan never fire on disconnect — and the Windows`npiperelay.exe`\n\nleaks under`wslhost`\n\n,one orphan per ssh/git operation, until you kill them by hand. Without`-p`\n\n, a busy pipe just makes the connection fail fast (retryable). See the troubleshooting note below.\n\nThe common `.zshrc`\n\napproach has every shell try to start/manage its own `socat`\n\n.\nThat causes:\n\n**Races** between shells spawning competing listeners.**Hangs at shell startup** if the block probes the agent with`ssh-add -l`\n\nwhile Bitwarden's single pipe instance is busy — the new terminal blocks forever.**Orphaned** processes left behind by closed terminals.`npiperelay.exe`\n\nA single systemd user service owns exactly one listener, survives across all terminals, restarts on failure, and starts on boot (with linger).\n\nFigure out **which side** is broken. The native Windows client talks straight to\nthe pipe — no WSL, socat, or npiperelay involved:\n\n```\n/mnt/c/Windows/System32/OpenSSH/ssh-add.exe -l\n```\n\n**It also hangs/fails → the problem is Bitwarden**, not WSL. See below.** It works but WSL doesn't → the problem is the relay**(socat/npiperelay path, flags, or socket). Check`journalctl --user -u bitwarden-ssh-agent`\n\n.\n\nBitwarden exposes a **single pipe instance**. If a client connection is\n**interrupted or force-killed** — a closed terminal mid-operation, a Ctrl-C'd\n`ssh-add`\n\n, or `Stop-Process -Force`\n\non `npiperelay.exe`\n\n— Bitwarden may fail to\nre-arm the listener. After that, *every* client (including native\n`ssh-add.exe`\n\n) hangs until you kick the agent:\n\n- Bitwarden → Settings → SSH agent → toggle\n**off, then on**; or **Fully quit** Bitwarden (tray → Quit) and reopen + unlock.\n\nThis is why this unit deliberately\n\ndoes notforce-kill orphaned`npiperelay.exe`\n\n— abruptly killing an in-flight pipe client is one of the things that wedges the agent.\n\nIf orphaned `npiperelay.exe`\n\never genuinely pile up, clear them by hand\n(PowerShell):\n\n```\nGet-Process npiperelay -ErrorAction SilentlyContinue | Stop-Process -Force\n```\n\nThis means another client momentarily holds Bitwarden's single pipe instance, or\nthe agent is wedged. **Do not \"fix\" it with -p** — that trades a fast, retryable\nerror for an unbounded orphan leak (see the flags note above). Instead:\n\n- If it's transient (two ssh ops at once), just retry.\n- If it persists with nothing connected, the agent is wedged — kick Bitwarden (toggle the SSH agent setting, or fully quit + reopen).\n- If you see orphaned\n`npiperelay.exe`\n\npiling up, you almost certainly have`-p`\n\nsomewhere — remove it.\n\nsystemd user services can normally launch Windows `.exe`\n\nfiles in recent WSL.\nVerify:\n\n```\nsystemd-run --user --wait --pipe /path/to/npiperelay.exe -h\n```\n\nIf that fails, your WSL build may not expose interop to systemd services; update\nWSL (`wsl --update`\n\n).\n\n```\nsystemctl --user status   bitwarden-ssh-agent\nsystemctl --user restart  bitwarden-ssh-agent\nsystemctl --user stop     bitwarden-ssh-agent\njournalctl  --user -u     bitwarden-ssh-agent -f\n```\n\n- Original walkthrough:\n[https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/](https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/) `npiperelay`\n\n:[https://github.com/albertony/npiperelay](https://github.com/albertony/npiperelay)(fork of jstarks/npiperelay)", "url": "https://wpnews.pro/news/bitwarden-ssh-agent-in-wsl2-via-a-systemd-user-service-socat-npiperelay-with-the", "canonical_source": "https://gist.github.com/eliottness/f144341ee3952493468dcab94da66313", "published_at": "2026-06-20 16:33:09+00:00", "updated_at": "2026-06-24 14:08:45.490974+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Bitwarden", "WSL2", "socat", "npiperelay", "Microsoft", "Windows", "OpenSSH"], "alternates": {"html": "https://wpnews.pro/news/bitwarden-ssh-agent-in-wsl2-via-a-systemd-user-service-socat-npiperelay-with-the", "markdown": "https://wpnews.pro/news/bitwarden-ssh-agent-in-wsl2-via-a-systemd-user-service-socat-npiperelay-with-the.md", "text": "https://wpnews.pro/news/bitwarden-ssh-agent-in-wsl2-via-a-systemd-user-service-socat-npiperelay-with-the.txt", "jsonld": "https://wpnews.pro/news/bitwarden-ssh-agent-in-wsl2-via-a-systemd-user-service-socat-npiperelay-with-the.jsonld"}}