← Back to dashboard

WSL — Linux on Windows Developer Guide

What is WSL?

The real model

WSL2 = Linux kernel in a managed VM; keep repos on the Linux fs, drive it with wsl.exe + interop.

Install with `wsl --install` (admin PowerShell) → Ubuntu on WSL 2 by default. Manage with `wsl -l -v`, `--set-version`, `--shutdown`, `--export/--import`. Tune the VM globally in `%UserProfile%\.wslconfig` ([wsl2] memory/processors/networkingMode=mirrored) and per-distro in `/etc/wsl.conf` ([boot] systemd=true). The rule that governs everything: store code in `/home/you/code`, never `/mnt/c` — cross-OS I/O is the one thing WSL1 beats WSL2 at, and on the Linux fs WSL2 is up to 20× faster unpacking tarballs, 2–5× on git/npm/cmake. It's the right place to run Claude Code on Windows: native Linux toolchain, Windows interop one `explorer.exe .` away.

Six things to know

The architecture, the golden rule, and the tooling it unlocks. Tap a card for commands + gotchas.

Where should your code live? — the filesystem advisor

WSL’s single biggest gotcha. Choose where your files live and which OS’s tools touch them — watch the path translation and the speed verdict flip. The lesson in one move: drag “files” to Windows while using Linux tools and watch it fall off the performance cliff.

Where do the files live?
Which OS's tools touch them?
Linux fs
~/code
⇄cross
Windows /mnt/c
C:\Users
● files here
Relative I/O speed≈ 2–20× slower
from Linux: /mnt/c/Users/you/code/your-repo
from Windows: C:\Users\you\code\your-repo
✗ Slow: The classic mistake. Linux tools on /mnt/c cross the OS boundary on every file op — 2–20× slower. Move the repo to ~/code.

The rule (store files on the filesystem matching your tools) and the 2–20× range are from Microsoft Learn (compare-versions, filesystems). The speed bar is relative/illustrative — actual gains depend on the workload. Apply it: keep repos in ~/codeand run Claude Code from there.

Text
██╗    ██╗███████╗██╗
██║    ██║██╔════╝██║
██║ █╗ ██║███████╗██║
██║███╗██║╚════██║██║
╚███╔███╔╝███████║███████╗
 ╚══╝╚══╝ ╚══════╝╚══════╝

WSL — Linux on Windows Developer Guide

Focus: Everything a developer needs to run Linux on Windows with WSL — install, architecture, Ubuntu distro management and first-run setup, the apt/Node toolchain for the codeAmani stack, the filesystem performance rule, the full command + config reference, systemd, Docker/GPU/USB/GUI, Windows Terminal, editors over Remote-WSL, and running Claude Code inside WSL. Grounded in learn.microsoft.com and Canonical's Ubuntu-on-WSL docs; reviewed 2026-08-23.

Good news: getting a real Ubuntu environment on Windows is now a one-command affair, and once you internalise a single rule (keep your code on the Linux side), it's genuinely fast and pleasant. Work through the diagrams below, copy the commands as you go, and you'll have a production-grade dev box in an afternoon. Let's dive in.

Table of Contents

  1. Overview & architecture
  2. Install & first run
  3. Which Ubuntu? Flavours, versions & the distro manifest
  4. Your Ubuntu user account
  5. WSL 1 vs WSL 2
  6. Distribution management
  7. The filesystem: interop & the performance rule
  8. The Ubuntu toolchain for the codeAmani stack
  9. Command reference
  10. Configuration: .wslconfig vs wsl.conf
  11. systemd & Linux services
  12. Interop: running Windows ↔ Linux
  13. Networking & ports
  14. Docker, databases, GPU, USB & GUI apps
  15. Windows Terminal & shell setup
  16. Editors: VS Code & Cursor over Remote-WSL
  17. Claude Code in WSL
  18. Disposable distros
  19. Automations & dotfiles
  20. Troubleshooting
  21. codeAmani notes

The interactive learn module above this page is a live filesystem-location advisor + WSL1/WSL2 explorer — start there for intuition, then use this reference.


Official Documentation


1. Overview & architecture

The Windows Subsystem for Linux lets you run an unmodified Linux distribution (Ubuntu, Debian, Kali, openSUSE, Arch, AlmaLinux, Fedora, …) directly on Windows — Linux apps, utilities, and Bash — without a traditional VM or dual-boot.

WSL 2 (the default) runs a genuine Linux kernel inside a lightweight utility VM, giving 100% system-call compatibility, systemd, Docker, and GPU compute. Each distro is an isolated container inside that shared managed VM — which is why wsl --shutdown stops all of them at once. Here's the whole picture in one diagram; notice the two filesystems, because that distinction decides your performance:

Prerequisites: Windows 11, or Windows 10 version 2004 / build 19041+ for the wsl --install command. (WSL 2 itself needs Windows 11 or Win 10 version 1903 / build 18362+.)

WSL ships from the Microsoft Store, not Windows Update. Since Windows 19044+, wsl --install pulls the Store-serviced package, so new WSL features land without waiting on an OS update. wsl --update keeps it current; wsl --update --web-download works where the Store is blocked by policy.


2. Install & first run

Open PowerShell as Administrator and run one command:

PowerShell
wsl --install

That single command does a surprising amount of work for you — here's the whole flow, start to finish:

This enables the WSL + Virtual Machine Platform components, downloads the latest Linux kernel, sets WSL 2 as default, and installs Ubuntu. Reboot when prompted. On first launch you create a Linux username + password (separate from Windows — see §4).

PowerShell
# Install a specific distro instead of the default Ubuntu
wsl --list --online                 # see installable distros (alias: wsl -l -o)
wsl --install -d Ubuntu-24.04       # pin a specific Ubuntu LTS
wsl --install -d Ubuntu --no-launch # install now, do first-run setup later
wsl --install --no-distribution     # install WSL itself, no distro yet

# Keep WSL itself up to date
wsl --update
wsl --version                       # confirm WSL / kernel / WSLg versions

Useful --install flags, all current:

FlagEffect
-d, --distribution <Name>Which distro to install (on Windows 11 the bare wsl --install Ubuntu-24.04 also works)
--no-launchInstall without running first-run setup
--web-downloadFetch from the web instead of the Microsoft Store
--location <Dir>Install the distro's VHD somewhere other than %LOCALAPPDATA% — use this to keep a large distro off a small C: drive
--from-file <file.wsl>Install a tar-based distro image you downloaded yourself (WSL 2.4.4+)
--no-distributionInstall the WSL platform only
--inboxUse the in-Windows component instead of the Store package (updates then come via Windows Update)
--enable-wsl1Also enable the legacy WSL 1 optional component

If wsl --install only prints the help text, WSL is already present — use wsl --install -d <Distro>. If a download hangs at 0.0%, add --web-download.

Immediately after first launch, update the distro. Windows never updates your Linux packages for you:

Bash
sudo apt update && sudo apt upgrade -y
lsb_release -dc          # confirm which Ubuntu release + codename you're on

3. Which Ubuntu? Flavours, versions & the distro manifest

wsl --list --online reads a manifest that groups distros by flavour, and each flavour has a default entry plus pinned versions. For Ubuntu that means these names are valid after -d:

Name to pass -dWhat it isUse it when
UbuntuThe flavour default — tracks the current Ubuntu LTS and follows it forward across point releasesDefault choice. This is what plain wsl --install gives you
Ubuntu-26.04Ubuntu 26.04 LTS, pinnedYou want a release that will not move under you
Ubuntu-24.04Ubuntu 24.04 LTS, pinnedMatching an existing prod base image / CI runner
Ubuntu-22.04Ubuntu 22.04 LTS, pinnedLegacy toolchain that hasn't been ported
Ubuntu-20.04Ubuntu 20.04 LTS, pinnedReproducing an old bug only

Canonical also publishes an Ubuntu (Preview) app that tracks the current development release — useful for testing, never for a machine you ship from.

PowerShell
wsl --list --online                 # confirm the exact names on YOUR machine
wsl --install -d Ubuntu-26.04       # pinned LTS
wsl --install -d Ubuntu             # rolling-LTS flavour default

Which should you pick? Match production. Our services run on Linux containers, so pin the Ubuntu LTS your Dockerfile's base image uses (FROM node:22-bookworm is Debian, FROM ubuntu:24.04 is 24.04) and you get genuine dev/prod parity. If nothing constrains you, take the flavour default Ubuntu and let it ride the LTS train.

Running two Ubuntus side by side is normal and cheap. Each is an independent container with its own ext4 VHD, users, and packages. A pinned Ubuntu-24.04 for a client project alongside Ubuntu for everything else costs disk, not complexity:

PowerShell
wsl --install -d Ubuntu-24.04
wsl -l -v                    # both listed, each with its own version + state
wsl -d Ubuntu-24.04          # launch the pinned one without changing the default
wsl --set-default Ubuntu     # decide which one bare `wsl` opens

Modern Ubuntu images are tar-based .wsl files, not Store .appx packages. You can download one from ubuntu.com/wsl and install it directly, which is the path to take on a locked-down machine where the Microsoft Store is unavailable:

PowerShell
wsl --install --from-file C:\Downloads\ubuntu-24.04.wsl
# ...or just double-click the .wsl file in File Explorer

4. Your Ubuntu user account

The first launch of any Ubuntu distro prompts for a UNIX username and password. This trips people up more than it should, so, precisely:

  • The account is per distro, not per machine — install Ubuntu twice and you create two accounts. It has no relationship to your Windows account.
  • Nothing appears on screen while you type the password. That's "blind typing" and it's normal, not a hung terminal.
  • The account you create becomes the distro's default user (auto-signed-in on launch) and is in sudo — it is the Linux administrator.
  • WSL distros are a per-Windows-user installation. Another Windows account on the same PC cannot see or share your distro.
Bash
passwd                       # change your own password
whoami                       # who am I actually running as?
id                           # uid/gid — matters for /mnt/c permission masks

Forgot the password? Get in as root from the Windows side and reset it:

PowerShell
wsl -d Ubuntu -u root        # root shell, no password needed
Bash
passwd yourusername          # set a new one, then `exit`

Change which user launches by default — two ways, depending on how the distro got installed:

PowerShell
# Store/launcher distros (Ubuntu, Ubuntu-24.04, ...)
ubuntu config --default-user johndoe
INI
# /etc/wsl.conf — works for EVERY distro, including imported ones,
# which have no launcher .exe and so cannot use `config --default-user`
[user]
default=johndoe

Then wsl --terminate Ubuntu (or wsl --shutdown) and relaunch — wsl.conf is read at distro start.


5. WSL 1 vs WSL 2

WSL 2 is the recommended default. The official feature comparison:

FeatureWSL 1WSL 2
Windows ↔ Linux integration✅✅
Fast boot, small footprint✅✅
Managed VM❌✅
Full Linux kernel❌✅
Full system-call compatibility❌✅
Performance across OS file systems✅❌
systemd support❌✅
Runs alongside current VMware/VirtualBox✅❌
IPv6✅✅

Read the table this way: WSL 2 wins everywhere except cross-OS file access — and you neutralise that by keeping files on the matching filesystem (§7). WSL 2 runs up to 20× faster unpacking a tarball and 2–5× faster on git clone / npm install / cmake than WSL 1 — when files live on the Linux side.

Pick WSL 1 only if: files must live on the Windows filesystem and you access them from Linux tools; you need a serial port (WSL 2 has no serial support — USB is covered by usbipd-win); or you have strict host-memory limits.

PowerShell
wsl -l -v                       # which version is each distro on?
wsl --set-version Ubuntu 2      # convert a distro to WSL 2
wsl --set-default-version 2     # default for new installs

Converting between versions rewrites the whole filesystem. On a distro with large projects, wsl --export first — Microsoft explicitly warns conversions can fail mid-flight.


6. Distribution management

PowerShell
wsl -l -v                                    # installed distros + version + state
wsl -l --running                             # only the ones currently up
wsl --set-default Ubuntu                     # set the default distro
wsl -d Ubuntu-24.04                          # launch a specific distro
wsl -d Ubuntu -u root                        # ...as a specific user
wsl ~                                        # open the default distro at $HOME

# Backup / clone / move a distro (export → import)
wsl --export Ubuntu D:\backups\ubuntu.tar    # snapshot to tar  (--vhd for .vhdx)
wsl --import UbuntuClone D:\wsl\clone D:\backups\ubuntu.tar
wsl --import-in-place Ubuntu-Old D:\wsl\ext4.vhdx   # adopt an existing ext4 VHD
wsl --unregister UbuntuClone                 # delete a distro + its disk (irreversible)

# Disk sizing (WSL 2.5+)
wsl --shutdown
wsl --manage Ubuntu --resize 256GB           # grow the ext4 VHD; see `wsl --manage --help`

--export/--import is your portable backup and the way to move a distro off the system drive. --unregister permanently deletes the distro's ext4 VHDX — back up first.

Disk space facts worth knowing. Each distro is an ext4.vhdx allocated a 1 TB maximum by default (512 GB / 256 GB on older WSL releases). The VHD grows on demand but does not shrink on its own when you delete files — that's why a distro that once held a big node_modules keeps eating disk. Enable sparse VHDs so new distros release freed space back to Windows:

INI
# %UserProfile%\.wslconfig
[experimental]
sparseVhd=true

Never touch the VHD from Windows. The files under %LOCALAPPDATA%\Packages\...\LocalState\ are the live Linux disk; editing them with Windows tools corrupts the distro. Reach Linux files through \\wsl.localhost\Ubuntu\... instead (§7).


7. The filesystem: interop & the performance rule

The single most important WSL habit. Get this one right and everything feels fast; get it wrong and you'll blame WSL for being slow when it's really the boundary crossing. Each filesystem is fast from its own OS and slow across the boundary:

Where the files arePath from LinuxPath from WindowsFast for
Linux filesystem (ext4 VHDX)/home/you/code\\wsl.localhost\Ubuntu\home\you\codeLinux tools ✅
Windows filesystem/mnt/c/Users/you/codeC:\Users\you\codeWindows tools ✅

✅ Keep your repos in the Linux filesystem (/home/you/...) when you work with Linux tooling (git, npm, build chains). Putting them on /mnt/c forces every file op across the OS boundary and is dramatically slower. \\wsl$ still works as an alias for \\wsl.localhost.

Bash
# Jump between worlds
explorer.exe .                       # open the current Linux dir in File Explorer
cd /mnt/c/Users/you/Downloads        # reach the Windows C: drive from Linux
# In Windows File Explorer address bar:  \\wsl.localhost   (or the older \\wsl$)
Bash
# Clone into the Linux fs (fast), NOT /mnt/c (slow)
mkdir -p ~/code && cd ~/code
git clone https://github.com/codeAmani-Solutions/your-repo.git

Windows drives mount through DrvFs, and its defaults matter. /mnt/c mounts with umask=022, fmask=000, dmask=000, and metadata disabled — which is why every file on /mnt/c looks 777 and chmod silently does nothing. Turning metadata on gives real Linux permissions on Windows files (and is what makes shell scripts on /mnt/c executable):

INI
# /etc/wsl.conf
[automount]
enabled=true
options="metadata,umask=22,fmask=11"
root=/mnt/

Two more cross-boundary hazards, both worth fixing on day one:

  • Case sensitivity. Linux is case-sensitive, Windows is not. A repo cloned on the Windows side can carry Foo.ts and foo.ts collisions that only surface in Linux. Clone on the Linux side and the question never arises.
  • CRLF line endings. Files touched by Windows editors arrive with \r\n and break shell scripts (bad interpreter: /bin/bash^M). Set git config --global core.autocrlf input inside Ubuntu and commit a .gitattributes.

8. The Ubuntu toolchain for the codeAmani stack

This is the sequence that turns a fresh Ubuntu into a box that can build our Next.js 15 / React 19 / TypeScript projects. Run it once, in order.

8.1 Base packages

Bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
  build-essential \
  git curl wget unzip \
  ca-certificates gnupg lsb-release \
  jq ripgrep fd-find

build-essential is not optional: it pulls in gcc, g++, and make, which any npm package with a native addon (sharp, better-sqlite3, node-gyp fallbacks) needs at install time. Installing it up front turns a whole class of confusing npm install failures into non-events.

8.2 Node.js — use nvm, not apt install nodejs

Microsoft's own guidance is explicit here: the Node in Ubuntu's apt repositories is outdated, and mixing an apt-installed Node with a version manager produces "strange and confusing conflicts". Remove any existing Node first, then install nvm:

Bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
exec $SHELL -l                 # or close and reopen the terminal
command -v nvm                 # should print "nvm"

nvm install --lts              # current LTS — what we build against
nvm install node               # optional: current release, for testing
nvm alias default lts/*        # LTS is what new shells get
node -v && npm -v

With nvm you never sudo npm install -g again — globals land in ~/.nvm, owned by you. Per-project switching works off .nvmrc:

Bash
echo "lts/*" > .nvmrc
nvm use                        # honours .nvmrc in the current repo
corepack enable                # pnpm / yarn shims, shipped with Node

Alternatives: fnm (faster, Rust), volta, n, asdf. All fine — pick one and only one. The failure mode is always two managers fighting over $PATH.

8.3 Git + credentials

Bash
git config --global user.name  "Your Name"
git config --global user.email "you@codeamani.com"
git config --global init.defaultBranch main
git config --global core.autocrlf input          # never write CRLF from Linux
git config --global pull.rebase true

Reuse the Windows Git Credential Manager so you are not pasting PATs into a Linux shell — GCM stores them in the Windows Credential Manager, encrypted per Windows account:

Bash
git config --global credential.helper \
  "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"

The GitHub CLI is worth having too — it authenticates once and covers PRs, issues, and releases:

Bash
sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
  | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null
sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
  | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update && sudo apt install -y gh
gh auth login

8.4 Verify the stack end to end

Bash
mkdir -p ~/code && cd ~/code
npx create-next-app@latest smoke-test --ts --app --tailwind --eslint
cd smoke-test && npm run dev

Open http://localhost:3000 in the Windows browser — localhost forwarding means it just works (§13). If npm install was fast and the dev server hot-reloads on save, your filesystem placement is right. If file watching misses changes, you are on /mnt/c — move the repo to ~/code.

Why file watching breaks on /mnt/c: inotify events do not propagate across the 9P boundary reliably, so Next.js Fast Refresh and tsc --watch go quiet. This is the same root cause as the slowness, and it has the same fix.


9. Command reference

All from PowerShell/CMD (use wsl.exe from inside Linux):

CommandDoes
wsl --install [-d <Distro>]Install WSL + a distro
wsl --install --from-file <x.wsl>Install a tar-based distro image (WSL 2.4.4+)
wsl --list --onlineList installable distros
wsl -l -vList installed distros, version, state
wsl -l --running / -l --quietOnly running distros / names only
wsl --set-version <Distro> <1|2>Convert a distro's WSL version
wsl --set-default-version 2Default version for new distros
wsl --set-default <Distro>Which distro bare wsl opens
wsl -d <Distro> [-u <User>]Launch a specific distro, optionally as a user
wsl ~Launch the default distro at $HOME
wsl --update [--web-download]Update WSL itself
wsl --version / wsl --statusWSL/kernel/WSLg versions · default distro + version
wsl --shutdownStop the WSL 2 VM + all distros (apply .wslconfig)
wsl --terminate <Distro>Stop one distro
wsl --export <Distro> <file>Snapshot a distro (--vhd for .vhdx)
wsl --import <Name> <dir> <file>Restore/clone a distro
wsl --import-in-place <Name> <vhdx>Adopt an existing ext4 VHD as a distro
wsl --unregister <Distro>Delete a distro + its disk
wsl --manage <Distro> --resize <size>Resize the distro's VHD (WSL 2.5+)
wsl --mount <DiskPath> / --unmountAttach/detach a physical or virtual disk
wsl hostname -IWSL 2 VM IP address
<Distro> config --default-user <User>Default login user (launcher distros only)

wslconfig.exe, bash.exe, and lxrun.exe are the deprecated originals. Everything is wsl/wsl.exe now.


10. Configuration: .wslconfig vs wsl.conf

Two files, two scopes:

Aspect.wslconfigwsl.conf
ScopeGlobal — the WSL 2 VM, all distrosPer-distro (works on WSL 1 too)
Location%UserProfile%\.wslconfig (Windows)/etc/wsl.conf (inside the distro)
ConfiguresRAM, CPUs, swap, networking, kernel, VHD sizeboot/systemd, automount, network, interop, default user, GPU, timezone
Applied bywsl --shutdown, then relaunchDistro restart (wsl --terminate <Distro>)
INI
# %UserProfile%\.wslconfig  — global WSL 2 VM tuning
[wsl2]
memory=8GB              # cap VM RAM (default: 50% of host)
processors=4            # logical CPUs (default: all)
swap=2GB                # default: 25% of memory, rounded up to the GB
localhostForwarding=true
guiApplications=true    # WSLg
networkingMode=mirrored # better VPN/IPv6 compatibility (Win11 22H2+)
dnsTunneling=true       # default true
firewall=true           # Hyper-V firewall applies Windows rules to WSL
autoProxy=true          # inherit the Windows HTTP proxy
defaultVhdSize=274877906944   # 256GB cap for NEW distros (default 1TB)

[experimental]
autoMemoryReclaim=gradual     # NOTE: experimental section, not [wsl2]
sparseVhd=true                # new VHDs shrink when files are deleted

⚠️ autoMemoryReclaim and sparseVhd live under [experimental], not [wsl2]. Put them in the wrong section and WSL silently ignores them — a config that looks right and does nothing.

INI
# /etc/wsl.conf  — per-distro settings
[boot]
systemd=true
command="service ssh start"    # runs as root at distro start (Win11 / Server 2022)

[automount]
enabled=true
options="metadata,umask=22,fmask=11"   # real Linux perms on /mnt/c files
root=/mnt/
mountFsTab=true

[network]
hostname=amani-dev
generateResolvConf=true        # false if you want to hand-write /etc/resolv.conf

[interop]
enabled=true                   # run Windows .exe from Linux
appendWindowsPath=true         # add Windows PATH to the Linux PATH

[user]
default=johndoe

[gpu]
enabled=true

[time]
useWindowsTimezone=true        # keeps the distro clock on the Windows timezone

The 8-second rule. Closing a distro window doesn't stop it — the subsystem keeps running for roughly 8 seconds. Editing wsl.conf, closing the window, and reopening it often reads the old config. Confirm with wsl -l --running ("There are no running distributions") before relaunching, or just use wsl --terminate <Distro> / wsl --shutdown.

WSL Settings is now a real GUI app in the Start menu, and Microsoft recommends it over hand-editing .wslconfig. Use it when you want a slider; use the file when you want it in your dotfiles repo.


11. systemd & Linux services

Ubuntu installed via wsl --install ships with systemd already enabled — no configuration needed. Check first before you go editing files:

Bash
systemctl list-unit-files --type=service   # works ⇒ systemd is running
ps -p 1 -o comm=                           # should print "systemd"

To enable it on a distro that doesn't have it (requires a Store-serviced WSL; wsl --version must be recognised):

Bash
sudo nano /etc/wsl.conf
INI
[boot]
systemd=true
PowerShell
wsl --shutdown        # from Windows, then relaunch the distro

Once systemd is PID 1, Ubuntu behaves like a normal server — which is exactly what you want for local Postgres, Redis, or an SSH daemon:

Bash
sudo systemctl enable --now postgresql
systemctl status postgresql
journalctl -u postgresql -n 50 --no-pager

Services enabled with systemctl enable start when the distro starts — which is the first time you open a shell in it, not when Windows boots. If you want a service up without opening a terminal, wsl -d Ubuntu -- true from a Windows startup task is enough to boot the distro.

For databases specifically — whether to run Postgres as a systemd service or as a Docker container in WSL — see the local-database guide.


12. Interop: running Windows ↔ Linux

Bash
# Run Windows programs from Linux (note the .exe)
explorer.exe .            # open current dir in File Explorer
code .                    # launch VS Code connected to WSL
clip.exe < file.txt       # copy file contents to the Windows clipboard
powershell.exe -c "Get-Date"
notepad.exe config.json
cmd.exe /C dir            # CMD builtins need cmd.exe /C

# Pipe across the boundary
cat report.csv | clip.exe
ls | findstr.exe ".log"   # Linux output → Windows tool
ipconfig.exe | grep IPv4 | cut -d: -f2
PowerShell
# Run Linux commands from Windows
wsl ls -la ~
wsl --cd ~ -- bash -lc "npm run build"
wsl grep -r "TODO" .
dir | wsl grep git

Windows executables invoked from Linux keep the WSL working directory, run as the active Windows user, and show up in Task Manager as if launched from CMD. Names are case-sensitive and must include .exe.

Share environment variables with WSLENV. It is a colon-separated list of variable names, each optionally suffixed with flags: /p translates a path between Windows and Linux form, /l marks a path list, /u sends it only Windows→WSL, /w only WSL→Windows.

PowerShell
# Windows side: make MY_TOKEN visible in WSL, and translate MY_DIR to a Linux path
setx WSLENV "MY_TOKEN/u:MY_DIR/p"

Interop is toggled per-distro in wsl.conf ([interop] enabled, appendWindowsPath). Turning appendWindowsPath=false off speeds up shell startup and tab completion noticeably, at the cost of losing bare code/explorer.exe — a reasonable trade if you script the few you need.


13. Networking & ports

WSL 2's default NAT mode forwards localhost between Windows and Linux, so a dev server on :3000 in Linux is reachable at http://localhost:3000 in your Windows browser. The VM gets its own IP that changes on restart, which is why you should reach services by localhost, never by the VM address.

Bash
npm run dev          # Next.js / Vite in WSL → Windows browser at localhost:3000
wsl hostname -I      # the WSL 2 VM IP, if you genuinely need it
ip route show | grep -i default | awk '{ print $3}'   # the Windows host, seen from WSL
INI
# %UserProfile%\.wslconfig — mirrored mode for VPNs / IPv6 / better localhost
[wsl2]
networkingMode=mirrored
dnsTunneling=true
autoProxy=true       # inherit Windows HTTP proxy

[experimental]
hostAddressLoopback=true     # let container↔host traffic use the host's own IPs
ignoredPorts=3000,5432       # let Linux bind these even if Windows uses them

Networking modes, current: nat (default), mirrored, virtioproxy, none, and bridged (deprecated since WSL 2.4.5 — don't start new setups on it). Since WSL 2.3.25, a failing NAT setup falls back to VirtioProxy automatically.

From Windows 11 22H2 + WSL 2.0.9+, Windows Firewall rules apply to WSL automatically (Hyper-V firewall, firewall=true). If a port that worked yesterday is refused today, check the Windows firewall before blaming WSL.


14. Docker, databases, GPU, USB & GUI apps

  • Docker — install Docker Desktop and enable the WSL 2 backend; the docker CLI then works inside every WSL distro with near-native performance (WSL 2's real kernel is what makes this possible). Keep bind-mounted source on the Linux side — a -v /mnt/c/...:/app mount reintroduces the whole /mnt/c penalty inside the container.
  • Databases — Postgres, MySQL, Redis, and MongoDB all run natively in Ubuntu under systemd, or as containers via Docker Desktop. Which to choose, plus connection strings that work from both Windows and Linux, is covered in local-database.
  • GPU compute — WSL 2 supports GPU paravirtualization (NVIDIA CUDA, DirectML) for ML/AI workloads; install the vendor's WSL driver on Windows, not inside Ubuntu. Installing a Linux GPU driver in the distro is the classic way to break it.
  • USB devices — not native; attach them with the usbipd-win project (usbipd attach --wsl). Serial ports remain WSL 1 only.
  • GUI apps (WSLg) — Linux GUI apps run out of the box (guiApplications=true); just sudo apt install and launch. Handy for gedit, GUI database clients, or a Linux Chrome for testing.
Bash
docker run --rm hello-world          # via Docker Desktop's WSL2 backend
nvidia-smi                           # confirm GPU passthrough (with the Windows driver)

15. Windows Terminal & shell setup

Windows Terminal auto-detects WSL distros as profiles (tabs, splits, themes) and creates a new profile whenever you install a distro. Set your Ubuntu as the default profile so a new terminal window is a Linux shell.

Two settings worth changing immediately in that profile:

  • Starting directory — set it to \\wsl$\Ubuntu\home\<you> (or leave it unset so the shell starts at $HOME). The default inherits the Windows working directory and drops you on /mnt/c/Users/..., which is exactly where you don't want to be.
  • Font — a Nerd Font if you want a prompt with glyphs.
Bash
# A typical first-hour shell setup inside Ubuntu
sudo apt install -y zsh
chsh -s $(which zsh)                  # optional: switch to zsh (takes effect next launch)
Bash
# ~/.bashrc (or ~/.zshrc) — quality-of-life helpers
alias e='explorer.exe .'
alias winhome='cd /mnt/c/Users/$USER'
alias dev='cd ~/code'
export BROWSER='/mnt/c/Program Files/Google/Chrome/Application/chrome.exe'

16. Editors: VS Code & Cursor over Remote-WSL

Install the WSL extension on Windows, then from an Ubuntu shell:

Bash
cd ~/code/your-repo
code .          # opens the editor as a client; the server runs inside WSL

The split matters: the UI runs on Windows, while extensions, the integrated terminal, the debugger, and language servers all execute in Linux against your Linux-filesystem files. Full speed, no /mnt/c penalty, and tsc/ESLint see the same paths CI does.

  • Extensions install per side. The Extensions pane splits into "Local — Installed" and "WSL: Ubuntu — Installed"; a linter installed locally does nothing for a WSL-opened folder. Install the language extensions on the WSL side.
  • Opening a repo through \\wsl$\Ubuntu\... from a plain Windows editor is not the same thing — the editor then runs Windows tooling over a network share, which is slow and gets pathing wrong. Use code . from inside the distro.
  • Full setup, settings sync, and debugging specifics live in vscode.
  • Cursor is a VS Code fork and inherits the same Remote-WSL architecture — cursor . from the distro behaves like code ., with its own server directory under ~/.cursor-server. Its AI features index the remote workspace, so the repo still belongs on the Linux side. See cursor.

17. Claude Code in WSL

WSL is an excellent home for Claude Code on a Windows machine: a native Linux toolchain (the environment most CLIs assume) with Windows interop a command away.

Bash
# Inside your Ubuntu distro — Node via nvm first (see §8.2)
nvm install --lts
npm install -g @anthropic-ai/claude-code   # see the claude-api guide for current install

cd ~/code/your-repo      # keep the repo on the LINUX fs for speed
claude                   # launch in the project

Why it clicks:

  • Toolchain parity — Bash, the GNU coreutils, and POSIX paths Claude Code's commands expect are all native. No Git-Bash-vs-PowerShell quoting archaeology, no MAX_PATH surprises.
  • Speed — with the repo in /home, file watching, git, and npm run at full speed (the §7 rule). Grep and glob over a big repo are a different order of magnitude than the same run over /mnt/c.
  • Global installs are yours — with nvm, npm install -g writes to ~/.nvm, so no sudo, no root-owned files in your home directory.
  • Interop for verification — Claude can explorer.exe . to show you a folder, or drive Windows tools, while staying in Linux. Pair with the chrome-devtools MCP to verify UIs in a real browser.

Bridging from a Windows-side agent: calling wsl.exe from a Windows shell works, but two things bite. Do not pass an env block (Windows env vars leak through WSLENV and mangle the Linux environment), and set MSYS_NO_PATHCONV=1 when invoking from Git Bash or it rewrites /home/you/... into a Windows path. Simplest is to run the agent inside the distro.

Run wsl --update periodically so the kernel + WSL features stay current for whatever you're building.


18. Disposable distros

Because a distro is just a tar file plus a registration, WSL is a genuinely good sandbox: install from an image, do something risky, --unregister, repeat. This is the cheapest isolation available on a Windows dev box.

PowerShell
# Spin up a throwaway Ubuntu from a snapshot of your clean baseline
wsl --export Ubuntu D:\wsl\baseline.tar                 # once, from a pristine distro
wsl --import scratch D:\wsl\scratch D:\wsl\baseline.tar
wsl -d scratch                                          # play here

# ...and burn it down
wsl --terminate scratch
wsl --unregister scratch

Harden the throwaway before running anything you don't trust:

INI
# /etc/wsl.conf inside the scratch distro
[interop]
enabled=false            # no launching Windows .exe from Linux
appendWindowsPath=false

[automount]
enabled=false            # no /mnt/c at all — Windows files are unreachable

That combination is the meaningful part: with interop off and automount off, code in the distro cannot reach the Windows filesystem or start Windows processes. It is not a security boundary against a determined attacker (it is still a shared VM and a shared kernel, and the network is wide open), but it is a solid blast-radius limiter for "run this unfamiliar install script". For stronger isolation and the full pattern catalogue, see sandbox.


19. Automations & dotfiles

Bash
# One-shot dev-box bootstrap (idempotent) — see examples/setup.sh for the full version
#!/usr/bin/env bash
set -euo pipefail
sudo apt-get update -y
sudo apt-get install -y git curl build-essential jq unzip ca-certificates
command -v nvm >/dev/null || curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
mkdir -p ~/code
echo "✅ dev box ready"
PowerShell
# Windows-side: a scheduled nightly WSL backup (Task Scheduler / PowerShell)
wsl --shutdown
wsl --export Ubuntu "D:\backups\ubuntu-$(Get-Date -Format yyyyMMdd).tar"
JSON
// .claude/settings.json — nudge keeping repos off /mnt/c
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [{ "type": "command",
          "command": "case \"$PWD\" in /mnt/c/*) echo 'You are under /mnt/c — Linux-fs (~/) is much faster for git/npm';; esac" }] }
    ]
  }
}

20. Troubleshooting

IssueFix
wsl --install only prints helpWSL already installed — use wsl --install -d <Distro>
Install hangs at 0.0%wsl --install --web-download -d <Distro>
Microsoft Store blocked by policywsl --install --web-download, or download a .wsl image and wsl --install --from-file
Everything feels slowRepo is on /mnt/c — move it to ~/ (the Linux fs)
File watching / Fast Refresh misses savesSame cause: inotify doesn't cross the /mnt/c boundary. Move the repo to ~/
Forgot the Ubuntu passwordwsl -d Ubuntu -u root, then passwd <username>
.wslconfig change ignoredRun wsl --shutdown, then relaunch. Confirm with wsl -l --running
autoMemoryReclaim / sparseVhd ignoredThey belong under [experimental], not [wsl2]
wsl.conf change ignoredThe 8-second rule — wsl --terminate <Distro> before relaunching
systemd not runningAdd [boot] systemd=true to /etc/wsl.conf, wsl --shutdown; needs a Store-serviced WSL
chmod on /mnt/c does nothingDrvFs has metadata off by default — add options="metadata,..." to [automount]
bad interpreter: /bin/bash^MCRLF line endings — git config --global core.autocrlf input, re-checkout
localhost:3000 unreachable from WindowsConfirm the server binds 0.0.0.0/localhost; try networkingMode=mirrored
VM eating RAMCap it: [wsl2] memory=8GB + [experimental] autoMemoryReclaim=gradual
Disk keeps growing after deleting filesThe ext4 VHD doesn't auto-shrink — set [experimental] sparseVhd=true for new distros
"No space left on device"wsl --manage <Distro> --resize <size> (WSL 2.5+) after wsl --shutdown
VPN breaks WSL networking[wsl2] networkingMode=mirrored + dnsTunneling=true
Can't access GPU/USBInstall the vendor WSL driver on Windows; for USB use usbipd-win
Clock drift after the host sleepssudo hwclock -s, or keep [time] useWindowsTimezone=true and systemd-timesyncd on
Distro corruptedRecover with [wsl2] safeMode=true, or restore from a wsl --export backup

21. codeAmani notes

  • Ubuntu is the distro. wsl --install, then pin if prod pins. Take the flavour default Ubuntu unless a project's container base image says otherwise, in which case install the matching Ubuntu-XX.04 and keep the two side by side. Matching the LTS your Dockerfile uses is the cheapest dev/prod parity available.
  • The performance rule is non-negotiable. Keep every repo in the Linux filesystem (~/code/...), never /mnt/c. On the I/O-heavy work we do daily — git, npm install, Next.js builds — that's the 2–20× difference Microsoft documents, plus working file watchers. The .claude hook in §19 nags you if you stray onto /mnt/c.
  • nvm, never apt install nodejs. Ubuntu's apt Node is old and conflicts with every version manager. nvm also removes the sudo npm install -g habit, which is how root-owned files end up in ~. Commit an .nvmrc to each repo so nvm use is deterministic.
  • Run Claude Code inside WSL. It gives the Linux toolchain our stack assumes (Bash, POSIX paths, native node tooling) while keeping Windows interop one explorer.exe . away. See claude-api for the current CLI install. If you must bridge from the Windows side, remember: no env block on wsl.exe, and MSYS_NO_PATHCONV=1 under Git Bash.
  • Secrets stay server-side. WSL doesn't change the rules — keep Stripe keys, and Daraja/M-Pesa credentials on Kenya-targeted projects, in .env.local (Linux side, git-ignored) or Hazina, never in shell history or committed dotfiles. Use Git Credential Manager so you're not pasting PATs into the Linux shell. Note that a .env.local on the Linux side is not protected from Windows — anything running as your Windows user can read it through \\wsl.localhost.
  • Cap resources on modest hardware. Many East-African dev machines are RAM-light; set [wsl2] memory= plus [experimental] autoMemoryReclaim=gradual so the WSL VM doesn't starve Windows. Add sparseVhd=true before you create distros, not after — it only applies to new VHDs.
  • Back up before anything destructive. wsl --export is fast and the tar is portable. wsl --unregister and wsl --set-version are both capable of losing a distro; a nightly export (§19) makes both survivable.
  • Mirror your prod target. Our services run on Linux (Cloud Run, containers). Developing in WSL means dev/prod parity — the same shell, the same paths, the same Docker — so "works on my machine" actually means it works.
GuideWhy it pairs with WSL
vscodeRemote-WSL setup, per-side extensions, debugging into the distro
cursorSame Remote-WSL split, AI indexing the Linux-side workspace
local-databasePostgres/Redis in WSL — systemd service vs Docker container
sandboxDisposable distros and stronger isolation patterns
chrome-devtoolsVerifying a WSL-hosted dev server in a real Windows browser