# Bitwarden SSH agent in WSL2 via a systemd user service (socat + npiperelay) — with the gotchas that make ssh-add hang

> Source: <https://gist.github.com/eliottness/f144341ee3952493468dcab94da66313>
> Published: 2026-06-20 16:33:09+00:00

Use the SSH keys stored in your **Windows Bitwarden Desktop** vault from inside
**WSL2** — `ssh`

, `git`

, `ssh-add`

, etc. — managed by a **systemd user service**
instead of a fragile `.zshrc`

snippet.

This bridges the Windows OpenSSH-agent named pipe that Bitwarden exposes
(`\\.\pipe\openssh-ssh-agent`

) to a Unix socket inside WSL using
[ npiperelay](https://github.com/albertony/npiperelay) +

`socat`

.It is a hardened version of the excellent walkthrough at
[https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/](https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/), with fixes for the
problems that make `ssh-add -l`

hang.

```
Windows                                   │ WSL2
                                          │
Bitwarden Desktop (standalone build)      │
  └─ \\.\pipe\openssh-ssh-agent  ◀────────┼──  npiperelay.exe  ◀──  socat  ◀──  ~/.ssh/agent.sock
       (one pipe instance)                │      (per connection)   (listener)     ▲
                                          │                                         │
                                          │                       ssh / git / ssh-add (SSH_AUTH_SOCK)
```

A systemd **user** service runs one persistent `socat`

listener. `socat`

forks a
short-lived `npiperelay.exe`

for each incoming connection, which relays bytes to
Bitwarden's named pipe.

The **Microsoft Store / Appx** build of Bitwarden Desktop runs in a UWP
AppContainer sandbox and **does not serve the SSH agent to external clients**.
The pipe appears and connections *open*, but Bitwarden never replies — so
everything hangs, including the native Windows `ssh-add.exe`

.

Check what you have (PowerShell):

```
Get-AppxPackage *Bitwarden*      # if this prints anything, you have the Store build — uninstall it
```

Fix — remove the Store/Appx build, then install the standalone build:

```
# Remove the Store (Appx) build. winget can't uninstall it by that name, so use Appx:
Get-AppxPackage *Bitwarden* | Remove-AppxPackage

# Install the standalone build (validated winget id):
winget install Bitwarden.Bitwarden
```

Or download the desktop installer from [https://bitwarden.com/download/](https://bitwarden.com/download/).

Bitwarden → **Settings → SSH agent → Enable SSH agent**. Have at least one SSH
key item in your vault, and keep the vault **unlocked** (the agent only answers
while unlocked).

It would fight Bitwarden for the same pipe name. PowerShell (admin):

```
Stop-Service ssh-agent
Set-Service ssh-agent -StartupType Disabled
```

- WSL2 with
**systemd enabled**—`/etc/wsl.conf`

:(then

```
[boot]
systemd=true
```

`wsl --shutdown`

from Windows once) `socat`

installed:`sudo apt install socat`

`npiperelay.exe`

on the Windows side. Easiest:then find the path (it lives under

```
winget install albertony.npiperelay
```

`C:\Users\<you>\AppData\Local\Microsoft\WinGet\Packages\albertony.npiperelay_*\npiperelay.exe`

).**Enable lingering** so the service runs without an interactive login:

```
loginctl enable-linger "$USER"
```

-
**Find your npiperelay path** and put it in`bw-ssh-relay`

(edit the`NPIPERELAY=`

line). From WSL:

```
ls /mnt/c/Users/*/AppData/Local/Microsoft/WinGet/Packages/albertony.npiperelay_*/npiperelay.exe
```

-
**Install the relay script:**

```
mkdir -p ~/.local/bin
cp bw-ssh-relay ~/.local/bin/bw-ssh-relay
chmod +x ~/.local/bin/bw-ssh-relay
```

-
**Install the service:**

```
mkdir -p ~/.config/systemd/user
cp bitwarden-ssh-agent.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now bitwarden-ssh-agent.service
```

-
**Point your shell at the socket.** Add to`~/.bashrc`

/`~/.zshrc`

:

```
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
```

-
**Test**(open a new shell):

```
ssh-add -l        # should list your Bitwarden SSH key(s)
```

| File | Goes to | Purpose |
|---|---|---|
`bw-ssh-relay` |
`~/.local/bin/bw-ssh-relay` |
Launches the `socat` ↔ `npiperelay` bridge (the service's `ExecStart` ). |
`bitwarden-ssh-agent.service` |
`~/.config/systemd/user/` |
systemd user unit that keeps one relay listener running and restarts it on failure. |

This is the upstream-article command: ** -ei -s** — nothing more.

| Flag | Meaning |
|---|---|
`-ei` |
Terminate when the client (stdin) closes — clean teardown per connection. This is what prevents process leaks, so it must be able to fire. |
`-s` |
Send a 0-byte message after EOF (part of the original recipe). |

⚠️

Do NOT addIt seems helpful ("poll until the pipe is free") but it is a trap with Bitwarden. Bitwarden exposes only`-p`

.onepipe instance; when it is busy or wedged,`-p`

makes npiperelay loop forever in`DialPipe`

. While polling it never reads stdin, so`-ei`

can never fire on disconnect — and the Windows`npiperelay.exe`

leaks under`wslhost`

,one orphan per ssh/git operation, until you kill them by hand. Without`-p`

, a busy pipe just makes the connection fail fast (retryable). See the troubleshooting note below.

The common `.zshrc`

approach has every shell try to start/manage its own `socat`

.
That causes:

**Races** between shells spawning competing listeners.**Hangs at shell startup** if the block probes the agent with`ssh-add -l`

while Bitwarden's single pipe instance is busy — the new terminal blocks forever.**Orphaned** processes left behind by closed terminals.`npiperelay.exe`

A single systemd user service owns exactly one listener, survives across all terminals, restarts on failure, and starts on boot (with linger).

Figure out **which side** is broken. The native Windows client talks straight to
the pipe — no WSL, socat, or npiperelay involved:

```
/mnt/c/Windows/System32/OpenSSH/ssh-add.exe -l
```

**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`

.

Bitwarden exposes a **single pipe instance**. If a client connection is
**interrupted or force-killed** — a closed terminal mid-operation, a Ctrl-C'd
`ssh-add`

, or `Stop-Process -Force`

on `npiperelay.exe`

— Bitwarden may fail to
re-arm the listener. After that, *every* client (including native
`ssh-add.exe`

) hangs until you kick the agent:

- Bitwarden → Settings → SSH agent → toggle
**off, then on**; or **Fully quit** Bitwarden (tray → Quit) and reopen + unlock.

This is why this unit deliberately

does notforce-kill orphaned`npiperelay.exe`

— abruptly killing an in-flight pipe client is one of the things that wedges the agent.

If orphaned `npiperelay.exe`

ever genuinely pile up, clear them by hand
(PowerShell):

```
Get-Process npiperelay -ErrorAction SilentlyContinue | Stop-Process -Force
```

This means another client momentarily holds Bitwarden's single pipe instance, or
the agent is wedged. **Do not "fix" it with -p** — that trades a fast, retryable
error for an unbounded orphan leak (see the flags note above). Instead:

- If it's transient (two ssh ops at once), just retry.
- If it persists with nothing connected, the agent is wedged — kick Bitwarden (toggle the SSH agent setting, or fully quit + reopen).
- If you see orphaned
`npiperelay.exe`

piling up, you almost certainly have`-p`

somewhere — remove it.

systemd user services can normally launch Windows `.exe`

files in recent WSL.
Verify:

```
systemd-run --user --wait --pipe /path/to/npiperelay.exe -h
```

If that fails, your WSL build may not expose interop to systemd services; update
WSL (`wsl --update`

).

```
systemctl --user status   bitwarden-ssh-agent
systemctl --user restart  bitwarden-ssh-agent
systemctl --user stop     bitwarden-ssh-agent
journalctl  --user -u     bitwarden-ssh-agent -f
```

- Original walkthrough:
[https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/](https://www.rebelpeon.com/bitwarden-ssh-agent-on-wsl2/) `npiperelay`

:[https://github.com/albertony/npiperelay](https://github.com/albertony/npiperelay)(fork of jstarks/npiperelay)
