cheat sheet

WSL Interoperability

Running Linux tools from Windows and vice versa, file system access, and networking between WSL and Windows.

WSL Interoperability

What it is

WSL Interop is the bridging layer that lets Windows and Linux talk to each other inside a WSL installation — Windows binaries are callable from a Linux shell as if they were native (notepad.exe README.md), Linux binaries are callable from PowerShell or CMD via the wsl <cmd> prefix, paths translate via wslpath, env vars cross the boundary via WSLENV, and the two filesystems are visible through /mnt/<drive> and \\wsl.localhost\<Distro>\. This page focuses on the bridge; for installing, listing, exporting, and managing distros themselves (the wsl.exe distro CLI), see the wsl cheat sheet. Reach for these techniques when you want to use Linux tools to massage Windows data (or vice versa) without leaving your current shell.

Run Linux commands from PowerShell

The wsl <command> prefix routes any Linux command through the default WSL distribution without opening a shell session. Windows stdin and stdout are piped transparently, so you can compose wsl calls with PowerShell pipelines.

powershell
# Run a Linux command directly
wsl ls -la /home/alice

# Pipe Windows command into Linux
Get-Content .\data.csv | wsl awk -F, '{ print $2 }'

# Use Linux grep on Windows output
ipconfig | wsl grep "IPv4"

# Run a specific distro
wsl -d Ubuntu-22.04 -- cat /etc/os-release

Output (wsl ls -la /home/alice):

text
total 48
drwxr-x--- 6 alice alice 4096 Apr 26 09:15 .
drwxr-xr-x 3 root  root  4096 Mar 10 12:00 ..
-rw-r--r-- 1 alice alice  220 Mar 10 12:00 .bash_logout
-rw-r--r-- 1 alice alice 3526 Mar 10 12:00 .bashrc
drwxr-xr-x 3 alice alice 4096 Apr 20 14:33 .config
drwxr-xr-x 2 alice alice 4096 Apr 25 10:02 projects

Output (Get-Content .\data.csv | wsl awk -F, '{ print $2 }'):

text
name
Alice
Bob
Carol

Output (ipconfig | wsl grep "IPv4"):

text
   IPv4 Address. . . . . . . . . . . : 192.168.1.42
   IPv4 Address. . . . . . . . . . . : 172.29.192.1

Output (wsl -d Ubuntu-22.04 -- cat /etc/os-release):

text
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"

Run Windows commands from WSL

From inside a WSL shell, Windows executables are accessible by their full name including the .exe extension — they live in the WSL PATH via the interop bridge. Use this to open Windows GUI apps, copy to the clipboard, or invoke PowerShell scripts without leaving your Linux terminal.

bash
# Call Windows executables (the .exe is required)
notepad.exe README.md
explorer.exe .
clip.exe < ~/.ssh/id_ed25519.pub   # copy SSH pubkey to clipboard

# Open Windows browser from WSL
cmd.exe /c start https://example.com

# Run PowerShell from WSL
powershell.exe -Command "Get-Date"
pwsh.exe -Command "Write-Host 'PowerShell 7'"

Output (powershell.exe -Command "Get-Date"):

text
Sunday, April 26, 2026 9:15:32 AM

Output (pwsh.exe -Command "Write-Host 'PowerShell 7'"):

text
PowerShell 7

File system paths

WSL mounts Windows drives under /mnt/<letter> (e.g. /mnt/c), and Windows can reach WSL files via the \\wsl.localhost\<Distro>\ UNC path. Use wslpath to convert between the two formats in scripts — hardcoding either style will break on the other side.

bash
# Access Windows drives from WSL
ls /mnt/c/Users/Alice/Documents

# Windows path to WSL path
wslpath 'C:\Users\Alice\Documents'        # → /mnt/c/Users/Alice/Documents
wslpath -w /home/alice/project            # → \\wsl.localhost\Ubuntu\home\alice\project

# Access WSL files from Windows Explorer
# Type in address bar: \\wsl.localhost\Ubuntu\home\alice

Output (wslpath 'C:\Users\Alice\Documents'):

text
/mnt/c/Users/Alice/Documents

Output (wslpath -w /home/alice/project):

text
\\wsl.localhost\Ubuntu\home\alice\project

Networking

In WSL 2, the Linux kernel runs in a lightweight VM with its own virtual network adapter, so Linux and Windows are on separate IP addresses — though localhost is bridged automatically for most dev-server use cases. WSL 1 shares the Windows network stack directly and has no bridging issues, but lacks the full kernel. Use netsh portproxy when you need to expose a WSL 2 service to other machines on the LAN.

bash
# Get Windows host IP from WSL (WSL2)
cat /etc/resolv.conf | grep nameserver | awk '{print $2}'
# or
ip route show default | awk '{print $3}'

# Expose WSL port to Windows (automatic in WSL2 for localhost)
# localhost:3000 in WSL → localhost:3000 in Windows browser

# Forward WSL port to LAN (run in PowerShell as Admin)
netsh interface portproxy add v4tov4 `
  listenport=3000 listenaddress=0.0.0.0 `
  connectport=3000 connectaddress=127.0.0.1

Output (cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):

text
172.29.192.1

Output (ip route show default | awk '{print $3}'):

text
172.29.192.1

Useful WSL management commands (PowerShell)

powershell
# List installed distros
wsl --list --verbose

# Set default distro
wsl --set-default Ubuntu-22.04

# Shutdown all WSL instances
wsl --shutdown

# Export distro to backup
wsl --export Ubuntu-22.04 ubuntu-backup.tar

# Import from backup
wsl --import Ubuntu-Restored C:\WSL\Ubuntu ubuntu-backup.tar

# Set WSL version for a distro
wsl --set-version Ubuntu-22.04 2

Output (wsl --list --verbose):

text
  NAME              STATE           VERSION
* Ubuntu-22.04      Running         2
  Ubuntu-20.04      Stopped         2
  Debian            Stopped         1

For full coverage of distro lifecycle (wsl --install, wsl --import, wsl --export, wsl --set-version, etc.), see the wsl cheat sheet.

How the interop bridge works

The interop bridge lives in two cooperating pieces: a Linux-side daemon called init (PID 1) that owns the bridge socket on /run/WSL/<pid>_interop, and a Windows-side driver (lxss.sys) plus the Wsl.Host user-mode helper. When a Linux process calls a .exe filename, binfmt_misc matches the magic bytes, hands the call off to init, which serialises the execve over the bridge socket; on the Windows side, Wsl.Host launches the executable and pipes stdio back. The reverse path uses NT's \Device\WSLInterop and the Wsl.exe host process.

Inspect the bridge sockets at runtime:

bash
ls -la /run/WSL/
# srwxrwxrwx 1 root root 0 May 24 09:00 12345_interop
# srwxrwxrwx 1 root root 0 May 24 09:01 12345_pty

# binfmt_misc entry that routes .exe to the interop layer
cat /proc/sys/fs/binfmt_misc/WSLInterop

Output:

text
enabled
interpreter /init
flags: PF
offset 0
magic 4d5a

/init is the bridge daemon — it is replaced by systemd on distros where systemd=true is set in /etc/wsl.conf, but the bridge logic still runs as a child of PID 1.

Disable interop at runtime (rarely useful but covered for completeness):

bash
# Inside WSL
sudo sh -c 'echo 0 > /proc/sys/fs/binfmt_misc/WSLInterop'

# Re-enable
sudo sh -c 'echo 1 > /proc/sys/fs/binfmt_misc/WSLInterop'

# Disable persistently via /etc/wsl.conf
cat <<'EOF' | sudo tee /etc/wsl.conf
[interop]
enabled = false
appendWindowsPath = false
EOF

Output:

text
[interop]
enabled = false
appendWindowsPath = false

appendWindowsPath = false is the bigger lever — it removes every Windows PATH entry from the Linux $PATH, which is what most "I don't want cmd.exe in my Linux shell" requests really need.

/etc/wsl.conf — per-distro interop settings

/etc/wsl.conf is the distro-side configuration file consumed by /init at boot. Sections affect interop, mounting behaviour, network handling, and systemd. The interop and automount sections are the most relevant for bridging.

ini
# /etc/wsl.conf
[automount]
enabled = true
options = "metadata,umask=22,fmask=11,uid=1000,gid=1000"
mountFsTab = true
root = /mnt/

[interop]
enabled = true
appendWindowsPath = true

[user]
default = alicedev

[boot]
systemd = true

[network]
hostname = mybox
generateHosts = true
generateResolvConf = true

[wsl2]
networkingMode = mirrored
firewall = true
dnsTunneling = true
autoProxy = true

options = metadata is the key flag for /mnt/c — without it, chmod and chown on a Windows drive are silently ignored. With metadata, WSL stores Unix permissions in NTFS alternate data streams.

Reload the file by terminating the distro and restarting it:

powershell
wsl --terminate Ubuntu-22.04
wsl -d Ubuntu-22.04

.wslconfig — global VM settings

.wslconfig is the Windows-side config file at %USERPROFILE%\.wslconfig controlling the whole WSL 2 VM (memory, CPU, swap, kernel, networking mode). Settings apply to every distro, take effect after wsl --shutdown, and have nothing to do with /etc/wsl.conf (which is per-distro). The interop-relevant settings are networkingMode, dnsTunneling, firewall, and autoProxy.

ini
# %USERPROFILE%\.wslconfig
[wsl2]
memory = 8GB
processors = 4
swap = 2GB
localhostForwarding = true
networkingMode = mirrored
firewall = true
dnsTunneling = true
autoProxy = true
guiApplications = true
nestedVirtualization = true

[experimental]
sparseVhd = true
autoMemoryReclaim = gradual

networkingMode = mirrored (Windows 11 22H2+) is the big one — it eliminates the dual-IP problem by making the Linux VM share Windows' network namespace, so localhost is shared in both directions and ifconfig inside WSL shows the same IPs as ipconfig does outside it. The legacy default is nat, which uses the bridged adapter and the localhost forwarding shim.

powershell
# Apply changes
wsl --shutdown
wsl

WSLENV — passing environment variables across the bridge

WSLENV is the environment variable that controls which env vars cross from Windows to Linux (and vice versa) when one side launches the other. It's a colon-separated list of NAME[/flags] entries. Without WSLENV, only a tiny default set crosses — anything else stays in its native shell.

The flag suffixes:

FlagDirectionMeaning
/pbothTreat as a path; translate C:\foo/mnt/c/foo
/lWindows → LinuxTreat as a path list (PATH-style); split on ; and translate each
/uWindows → Linux onlyPass only when Windows launches Linux
/wLinux → Windows onlyPass only when Linux launches Windows

Set it on the Windows side:

powershell
# Persist for the user
setx WSLENV "USERPROFILE/up:GOPATH/l:HOME/wu"

# Or just for the session
$env:WSLENV = "USERPROFILE/up:GOPATH/l:HOME/wu"

Then in a freshly-launched WSL shell:

bash
echo $USERPROFILE        # → /mnt/c/Users/alicedev
echo $GOPATH             # → /mnt/c/Users/alicedev/go:/mnt/d/go (translated)

Output:

text
/mnt/c/Users/alicedev
/mnt/c/Users/alicedev/go:/mnt/d/go

A typical recipe — pass an API token from Windows into Linux without leaking it onto the command line:

powershell
$env:GH_TOKEN = 'ghp_*****'
$env:WSLENV   = 'GH_TOKEN/u'
wsl gh repo list

Output:

text
alicedev/myproject  Public  Personal cheat-sheet site
...

Path translation with wslpath

wslpath is the canonical tool for converting between Windows paths (C:\Users\alice), WSL absolute paths (/mnt/c/Users/alice), and UNC paths (\\wsl.localhost\Ubuntu\home\alice). Use it in scripts whenever one side hands a path to the other — hardcoding either style breaks the moment the script runs on a different distro mount.

FlagDirectionOutput
(no flag)Win → POSIXC:\Users\alice/mnt/c/Users/alice
-uWin → POSIXSame as default; explicit
-wPOSIX → Win/home/alice\\wsl.localhost\Ubuntu\home\alice
-mPOSIX → Win (mixed, forward slashes)/mnt/c/UsersC:/Users — friendly to Cygwin-style tools
-aforce absoluteresolves ./relative before converting
bash
wslpath 'C:\Users\Alice\Documents'         # → /mnt/c/Users/Alice/Documents
wslpath -w /home/alicedev/project           # → \\wsl.localhost\Ubuntu\home\alicedev\project
wslpath -m /mnt/c/Windows/System32          # → C:/Windows/System32
wslpath -a ../relative                      # → /home/alicedev/relative

# Wrap a Windows variable
echo "$(wslpath "$USERPROFILE")"            # → /mnt/c/Users/alicedev

Output:

text
/mnt/c/Users/Alice/Documents
\\wsl.localhost\Ubuntu\home\alicedev\project
C:/Windows/System32
/home/alicedev/relative
/mnt/c/Users/alicedev

PowerShell does not have a built-in inverse, but you can call wslpath from the Windows side too:

powershell
wsl wslpath -w "/home/alicedev/project"

Output:

text
\\wsl.localhost\Ubuntu\home\alicedev\project

/mnt/c — DrvFs and the metadata flag

/mnt/<letter> is a DrvFs mount — a 9P-over-Hyper-V-Sockets filesystem driver that proxies all I/O back to NTFS on the Windows host. By default, DrvFs ignores Unix permissions (every file looks like 0777 owned by the current user) because NTFS has no native concept of Unix mode bits. The metadata mount option stores those bits in NTFS alternate data streams.

Mount with metadata so Unix permissions survive across reboots:

ini
# /etc/wsl.conf
[automount]
options = "metadata,umask=22,fmask=11,case=off,uid=1000,gid=1000"
OptionMeaning
metadataPersist Unix mode/uid/gid via NTFS streams
umask=22Default mask for new directories (0755)
fmask=11File mask offset (combined with umask gives 0644)
uid=1000,gid=1000Owner of files without metadata
`case=offdir

After changing [automount], wsl --shutdown and restart. Confirm metadata is active:

bash
mount | grep '^C:'

Output:

text
C: on /mnt/c type drvfs (rw,noatime,uid=1000,gid=1000,metadata,case=off,umask=22,fmask=11)

Performance note: DrvFs (/mnt/c) is much slower than the native ext4 filesystem inside the WSL VM. Keep source trees under /home/<user>/, not /mnt/c/Users/<user>/. The 9P trip for every stat-heavy operation (think npm install, git status on a 10000-file repo) typically runs 5–20× slower on /mnt/c.

\wsl.localhost\ — accessing Linux files from Windows

\\wsl.localhost\<Distro>\<path> is the reverse direction — Windows reads ext4 files inside a running WSL distro through a Plan 9 filesystem server hosted by init. Use it from Explorer, from any Win32 app that accepts UNC paths, or in PowerShell scripts.

powershell
# Open the WSL home in Explorer
explorer.exe \\wsl.localhost\Ubuntu\home\alicedev

# Read a file from PowerShell
Get-Content \\wsl.localhost\Ubuntu\home\alicedev\.bashrc

# Use a UNC path with native Windows tools
robocopy \\wsl.localhost\Ubuntu\home\alicedev\project C:\Backup\project /MIR

The legacy form \\wsl$\Ubuntu\... still works but is deprecated; new docs and IDEs prefer \\wsl.localhost\. The mapping is identical.

A common gotcha: Windows file watchers (FileSystemWatcher, used by every JS bundler in watch mode) do not receive change events on \\wsl.localhost\ paths. If hot-reload doesn't fire, the project lives on the wrong side of the bridge — move it under /home/ and edit it from VS Code's WSL remote.

Calling Windows from WSL — cmd.exe, PowerShell, browsers

Any Windows binary on %PATH% is invokable from WSL by its .exe name. The interop layer translates argv, stdio, and exit codes; WSLENV-tagged environment variables also cross. Three callable styles cover virtually every use case.

bash
# Direct exe call — arguments pass through verbatim
notepad.exe README.md
code.exe .
explorer.exe .                  # open current dir in Explorer

# Open a URL in the Windows default browser
cmd.exe /c start https://example.com

# Pipe to the Windows clipboard
echo "hello" | clip.exe
cat ~/.ssh/id_ed25519.pub | clip.exe

# Run a PowerShell command and capture its output
TIMEZONE=$(powershell.exe -NoProfile -Command '[System.TimeZoneInfo]::Local.Id' | tr -d '\r')
echo "Windows timezone: $TIMEZONE"

# Trigger a Windows toast
powershell.exe -Command 'New-BurntToastNotification -Text "Build complete"'

Output:

text
Windows timezone: Pacific Standard Time

cmd.exe /c start is the universal "open this with Windows' default handler" pattern — it works for URLs, files, anchor links, anything Explorer's Open With recognises.

Strip CR characters when piping Windows output into Linux tools — PowerShell terminates lines with \r\n, which trips up grep, awk, and cut:

bash
powershell.exe -Command 'Get-ChildItem C:\Windows -Filter *.dll | Select-Object -ExpandProperty Name' \
    | tr -d '\r' \
    | head -5

Output:

text
aadauthhelper.dll
aadcloudap.dll
aadjcsp.dll
AadWamExtension.dll
abovelockapp.dll

Calling WSL from Windows — wsl.exe -e / --exec

From PowerShell or CMD, wsl <command> runs a Linux command in the default distro; wsl -e (or --exec) skips the login shell and /etc/profile initialisation for a faster, more predictable invocation.

powershell
# Default invocation — uses the default distro and a login shell
wsl ls -la /home/alicedev

# Specific distro
wsl -d Ubuntu-22.04 -- cat /etc/os-release

# Specific user (sudo-free way to do work as root)
wsl -u root apt update

# Set working directory before running
wsl --cd /home/alicedev/project -- git status

# Skip the login shell — much faster for one-shots
wsl --exec /usr/bin/python3 -V
wsl -e /bin/bash -c "rg --type py 'def main' /home/alicedev/project"

# Pipe Windows command into Linux
Get-Content .\data.csv | wsl awk -F, '{ print $2 }'

# Pipe Linux back into PowerShell
$lines = wsl grep -c '^Error' /var/log/syslog
Write-Host "Error count: $lines"

The -- separator is important when the Linux command has flags that look like wsl.exe flags — without it, wsl ls -la / works, but wsl ls --color fails because --color is parsed by wsl.exe. Always wsl -- ls --color for safety.

A common pipeline pattern: use Linux tooling to summarise a Windows log:

powershell
Get-Content C:\Logs\app.log | wsl -e awk '/ERROR/ { count++ } END { print count }'

Output:

text
148

Named pipes and Unix sockets

WSL 2 supports Unix domain sockets (AF_UNIX) inside the VM, but Windows AF_UNIX sockets cannot be reached directly from Linux — and vice versa. The two interop paths for socket-style IPC are Hyper-V Sockets (AF_VSOCK on Linux, AF_HYPERV on Windows) and named pipes via \\.\pipe\<name> on Windows.

The most common practical case is the Docker Desktop socket: Docker Desktop exposes the Engine on a Windows named pipe at \\.\pipe\docker_engine and proxies it into WSL at /var/run/docker.sock. From Linux:

bash
docker ps                                    # uses the proxied socket
file /var/run/docker.sock                    # → socket

Output:

text
CONTAINER ID   IMAGE          COMMAND                  STATUS         PORTS                  NAMES
8f3c9a1b4d7e   nginx:alpine   "/docker-entrypoint.…"   Up 2 hours     0.0.0.0:8080->80/tcp   web
/var/run/docker.sock: socket

Without Docker Desktop, you can roll your own bridge using socat and the npiperelay Windows helper from jstarks/npiperelay:

bash
# Install socat in the distro
sudo apt install socat

# Place npiperelay.exe somewhere on $PATH (e.g. /usr/local/bin/)
# Then forward a Windows named pipe to a Unix socket
socat UNIX-LISTEN:/tmp/docker.sock,fork,group=docker,umask=007 \
    EXEC:"npiperelay.exe -ep -s //./pipe/docker_engine",nofork

Output: (none — socat blocks while forwarding; logs go to stderr)

For the reverse direction (Linux socket → Windows app), socat can listen on a TCP port and Windows connects to localhost:<port> (works trivially with networkingMode = mirrored).

SSH agent forwarding into WSL

A frequent ask: use your Windows OpenSSH agent (and Yubikey-backed keys) from inside WSL without re-importing the keys. npiperelay again — pair it with socat and a Unix socket override.

bash
# In ~/.bashrc
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
ss -a | grep -q "$SSH_AUTH_SOCK" || (
    rm -f "$SSH_AUTH_SOCK"
    setsid nohup socat UNIX-LISTEN:"$SSH_AUTH_SOCK",fork \
        EXEC:"npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork \
        >/dev/null 2>&1 &
)

Output: (none — exits 0; socat runs detached as a background process)

Now ssh-add -l inside WSL lists the keys loaded into the Windows ssh-agent service — see the windows ssh cheat sheet for managing that agent.

WSLg — running Linux GUIs on Windows

WSLg is Microsoft's compositor that lets a Linux X11 or Wayland client display windows directly on the Windows desktop. It ships as part of WSL on Windows 11 (and Windows 10 with the Microsoft.WSL MSIX package). No extra X server is needed — the user-mode RDP client mstsc.exe quietly handles the framebuffer, and wslg runs a Weston compositor inside a sidecar VM.

bash
# Confirm WSLg is wired up
echo $DISPLAY $WAYLAND_DISPLAY $XDG_RUNTIME_DIR
# Expected: :0  wayland-0  /run/user/1000

# Sockets WSLg mounts in
ls /mnt/wslg
# Tmp/  doc/  runtime-dir/  versions.txt  weston.log  wsl.log

# Launch a GUI app
sudo apt install -y x11-apps
xeyes &
xclock &

Output:

text
:0 wayland-0 /run/user/1000
Tmp  doc  runtime-dir  versions.txt  weston.log  wsl.log

A common dev-workflow use is opening Linux GUI tools from inside WSL alongside Windows IDE windows:

bash
gnome-disks &
gvim README.md &
firefox https://localhost:5173 &

Output: (none — apps launch as detached background jobs; window IDs printed to stderr)

Disable WSLg for headless servers via .wslconfig:

ini
[wsl2]
guiApplications = false

Or per-distro via /etc/wsl.conf:

ini
[boot]
guiApplications = false

Networking between Windows and WSL

In WSL 2 with the legacy nat networking mode, the VM has its own IP and Windows runs a small port forwarder for localhost. With networkingMode = mirrored (Windows 11 22H2+), the VM shares Windows' network namespace and the two IPs collapse into one.

bash
# Mirrored mode — same IP as Windows
hostname -I
ip addr show eth0
# Should match `ipconfig` output on Windows

# NAT mode — separate VM-side IP
# Discover the Windows host IP from Linux
ip route show default | awk '{print $3}'
cat /etc/resolv.conf | grep nameserver | awk '{print $2}'

Output (mirrored mode):

text
192.168.1.42

Output (NAT mode):

text
172.29.192.1

When you need a WSL-hosted service reachable from other machines on the LAN (NAT mode only — mirrored handles it automatically), use netsh portproxy:

powershell
# As Administrator
netsh interface portproxy add v4tov4 `
    listenport=3000 listenaddress=0.0.0.0 `
    connectport=3000 connectaddress=127.0.0.1

# Open the Windows Firewall for the port
New-NetFirewallRule -DisplayName "WSL Dev 3000" -Direction Inbound `
    -LocalPort 3000 -Protocol TCP -Action Allow

# List active portproxy rules
netsh interface portproxy show all

# Remove the rule when done
netsh interface portproxy delete v4tov4 listenport=3000 listenaddress=0.0.0.0

DNS resolution is handled by dnsTunneling (in .wslconfig) — when enabled, WSL hands DNS queries to Windows via Hyper-V Sockets rather than over UDP, which fixes split-DNS and VPN scenarios that used to require manual resolv.conf editing.

Clipboard and toast notifications

Two small bridges that punch above their weight in daily workflows: the Windows clipboard via clip.exe/Get-Clipboard, and Windows toast notifications via BurntToast or PowerShell directly.

bash
# Copy Linux output to the Windows clipboard
ls -la | clip.exe
cat ~/.ssh/id_ed25519.pub | clip.exe
date '+%Y-%m-%d %H:%M' | clip.exe

# Pull from the Windows clipboard into Linux
content=$(powershell.exe -Command 'Get-Clipboard' | tr -d '\r')
echo "Clipboard: $content"

# Toast notification on long-running task completion
make build && powershell.exe -Command \
    "[reflection.assembly]::LoadWithPartialName('System.Windows.Forms') | Out-Null;
     [System.Windows.Forms.MessageBox]::Show('Build complete')"

Output:

text
Clipboard: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@example.com

For richer notifications (icon, actions, dismissable), pair with the BurntToast PowerShell module:

bash
powershell.exe -Command "Import-Module BurntToast; New-BurntToastNotification -Text 'Build complete', 'All tests passed' -Sound 'Default'"

Output: (none — toast appears in the Windows Action Center)

Interop performance tips

Interop calls have non-trivial overhead — each .exe invocation crosses the Hyper-V VM boundary. A few rules keep WSL dev workflows snappy:

  1. Keep source on ext4. /home/<user>/project is 5–20× faster than /mnt/c/Users/<user>/project for stat-heavy ops.
  2. Use --exec for one-shot calls. wsl --exec skips the login shell, halving startup latency.
  3. Batch over the bridge. A single wsl ... ; ... ; ... is faster than three separate wsl calls.
  4. Prefer localhost to specific IPs. Mirrored mode resolves it locally; NAT mode falls back to the forwarder.
  5. Pin the kernel. WSL kernel updates can change 9p and Hyper-V Socket behaviour; lock to a tested version via [wsl2] kernel = C:\path\to\kernel.
  6. Avoid Windows file watchers on WSL paths. Bundlers that watch \\wsl.localhost\... will silently miss events; open the project from inside WSL via VS Code's Remote-WSL extension instead.

Common pitfalls

  1. /mnt/c permissions look like 0777 — add metadata to [automount].options in /etc/wsl.conf and wsl --shutdown.
  2. appendWindowsPath = true clutters $PATH — turn it off in /etc/wsl.conf if you don't want cmd.exe, PowerShell.exe, and 30 other Windows tools in which results.
  3. CR/LF endings break shell scripts on /mnt/c — files edited in Notepad get \r\n; bash reads the \r as part of the command. Use dos2unix or save via VS Code with LF endings.
  4. networkingMode = mirrored breaks Docker Desktop — Docker uses bridged networking and needs nat. Use a separate distro for Docker workloads or keep mirrored off.
  5. wsl.exe is not on the Windows PATH inside a Linux subshell — when called from another Linux program, the interop bridge needs an absolute path: /mnt/c/Windows/System32/wsl.exe.
  6. Environment variables don't cross by default — only a tiny default set crosses; populate WSLENV with the names you need.
  7. WSLg can't show windows for root — WSLg uses the default user's socket; sudo xeyes fails unless you add xhost +SI:localuser:root or run as the regular user.

Real-world recipes

One-line URL opener

Open the Windows browser to a URL from inside WSL — replaces xdg-open on a system without a GUI.

bash
# Add to ~/.bashrc
open() { cmd.exe /c start "$1"; }

open https://example.com
open "$(wslpath -w ./report.html)"

Output: (none — Windows opens the URL/file in the default handler)

Edit a Windows config file with vim, save via metadata

bash
sudo vim /mnt/c/Windows/System32/drivers/etc/hosts  # needs elevated WSL or write-protected

Output: (none — opens an interactive vim session)

For files that require Administrator, open the file from PowerShell (elevated) instead.

Sync a Windows folder into the WSL home

Faster builds by relocating a project from /mnt/c/ into /home/:

bash
rsync -a --delete \
    /mnt/c/Users/alicedev/Code/myproject/ \
    /home/alicedev/myproject/

Output: (none — exits 0 on success; stats only with -v)

Use Windows OpenSSH keys directly from WSL

bash
# Symlink the Windows .ssh dir into WSL
ln -s /mnt/c/Users/alicedev/.ssh ~/.ssh-win
chmod 700 ~/.ssh-win
ssh -i ~/.ssh-win/id_ed25519 alicedev@myhost

Output:

text
Linux myhost 6.1.0-21-amd64 #1 SMP Debian 6.1.90-1 (2026-04-01) x86_64
Last login: Sat May 25 09:14:22 2026 from 10.0.0.5
alice@myhost:~$

Or use ssh-agent forwarding via npiperelay (see above).

Cross-shell scripted dev-loop

Run Vitest in Linux, surface failures as Windows toasts:

bash
while true; do
    if ! pnpm test; then
        powershell.exe -Command "New-BurntToastNotification -Text 'Tests failed'"
    fi
    inotifywait -re modify ./src ./tests
done

Output: (loop runs indefinitely; each iteration prints pnpm test output)

Translate a path and open Explorer there

bash
# In ~/.bashrc
explore() { explorer.exe "$(wslpath -w "$(realpath "${1:-.}")")"; }

explore .
explore ../sibling-folder

Output: (none — Explorer opens the translated path in a new window)

2026 update notes

WSL has shipped meaningfully since the days of the in-OS-bundled v1: it is now distributed as an MSIX (Microsoft.WSL) and gets release-train updates independent of Windows. A few changes worth knowing about in 2026:

  • January 2026 servicing update fixed a mirrored networking regression that produced No route to host errors when accessing corporate resources over VPN — if you saw VPN-side traffic break in late 2025, the fix landed in the WSL 2.3.x release train.
  • dxgkrnl GPU patches (March 2026) added compute-only GPU support for running local LLMs, multiple virtual GPUs per VM, and dma-fence buffer sharing — relevant for wslg and CUDA workloads.
  • DrvFs metadata storage migration: recent WSL builds now persist Linux UID/GID/mode bits in NTFS Extended Attributes (EAs) rather than alternate data streams. Behaviour from the user's perspective is the same — the metadata mount option is still the switch — but files created on newer WSL versions and copied to older ones lose their permissions.
  • Mirrored networking conflict with Docker Desktop remains current: Docker's vpnkit and WSL mirrored mode both try to bind the same Windows host ports. Keep networkingMode = nat if Docker Desktop is the primary container runtime, or run Docker inside a separate distro without mirrored.

Track changes at the microsoft/WSL GitHub releases page — the legacy learn.microsoft.com/.../wsl/release-notes page has not been updated past 2021.

Sources