WSL — Linux on Windows Developer Guide
What is WSL?
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.
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.
██╗ ██╗███████╗██╗
██║ ██║██╔════╝██║
██║ █╗ ██║███████╗██║
██║███╗██║╚════██║██║
╚███╔███╔╝███████║███████╗
╚══╝╚══╝ ╚══════╝╚══════╝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.comand 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
- Overview & architecture
- Install & first run
- Which Ubuntu? Flavours, versions & the distro manifest
- Your Ubuntu user account
- WSL 1 vs WSL 2
- Distribution management
- The filesystem: interop & the performance rule
- The Ubuntu toolchain for the codeAmani stack
- Command reference
- Configuration:
.wslconfigvswsl.conf - systemd & Linux services
- Interop: running Windows ↔ Linux
- Networking & ports
- Docker, databases, GPU, USB & GUI apps
- Windows Terminal & shell setup
- Editors: VS Code & Cursor over Remote-WSL
- Claude Code in WSL
- Disposable distros
- Automations & dotfiles
- Troubleshooting
- 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
| Resource | URL |
|---|---|
| WSL docs (root) | https://learn.microsoft.com/windows/wsl/ |
| Install WSL | https://learn.microsoft.com/windows/wsl/install |
| Basic commands | https://learn.microsoft.com/windows/wsl/basic-commands |
| Set up a dev environment (first run) | https://learn.microsoft.com/windows/wsl/setup/environment |
| Comparing WSL 1 & 2 | https://learn.microsoft.com/windows/wsl/compare-versions |
| Working across filesystems | https://learn.microsoft.com/windows/wsl/filesystems |
| Advanced settings (config) | https://learn.microsoft.com/windows/wsl/wsl-config |
| systemd support | https://learn.microsoft.com/windows/wsl/systemd |
| Networking | https://learn.microsoft.com/windows/wsl/networking |
| Managing disk space | https://learn.microsoft.com/windows/wsl/disk-space |
| Node.js on WSL | https://learn.microsoft.com/windows/dev-environment/javascript/nodejs-on-wsl |
| Ubuntu on WSL (Canonical) | https://documentation.ubuntu.com/wsl/ |
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:
wsl --installThat 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).
# 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 versionsUseful --install flags, all current:
| Flag | Effect |
|---|---|
-d, --distribution <Name> | Which distro to install (on Windows 11 the bare wsl --install Ubuntu-24.04 also works) |
--no-launch | Install without running first-run setup |
--web-download | Fetch 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-distribution | Install the WSL platform only |
--inbox | Use the in-Windows component instead of the Store package (updates then come via Windows Update) |
--enable-wsl1 | Also enable the legacy WSL 1 optional component |
If
wsl --installonly prints the help text, WSL is already present — usewsl --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:
sudo apt update && sudo apt upgrade -y
lsb_release -dc # confirm which Ubuntu release + codename you're on3. 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 -d | What it is | Use it when |
|---|---|---|
Ubuntu | The flavour default — tracks the current Ubuntu LTS and follows it forward across point releases | Default choice. This is what plain wsl --install gives you |
Ubuntu-26.04 | Ubuntu 26.04 LTS, pinned | You want a release that will not move under you |
Ubuntu-24.04 | Ubuntu 24.04 LTS, pinned | Matching an existing prod base image / CI runner |
Ubuntu-22.04 | Ubuntu 22.04 LTS, pinned | Legacy toolchain that hasn't been ported |
Ubuntu-20.04 | Ubuntu 20.04 LTS, pinned | Reproducing 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.
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 defaultWhich 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:
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` opensModern 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:
wsl --install --from-file C:\Downloads\ubuntu-24.04.wsl
# ...or just double-click the .wsl file in File Explorer4. 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.
passwd # change your own password
whoami # who am I actually running as?
id # uid/gid — matters for /mnt/c permission masksForgot the password? Get in as root from the Windows side and reset it:
wsl -d Ubuntu -u root # root shell, no password neededpasswd yourusername # set a new one, then `exit`Change which user launches by default — two ways, depending on how the distro got installed:
# Store/launcher distros (Ubuntu, Ubuntu-24.04, ...)
ubuntu config --default-user johndoe# /etc/wsl.conf — works for EVERY distro, including imported ones,
# which have no launcher .exe and so cannot use `config --default-user`
[user]
default=johndoeThen
wsl --terminate Ubuntu(orwsl --shutdown) and relaunch —wsl.confis read at distro start.
5. WSL 1 vs WSL 2
WSL 2 is the recommended default. The official feature comparison:
| Feature | WSL 1 | WSL 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.
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 installsConverting between versions rewrites the whole filesystem. On a distro with large projects,
wsl --exportfirst — Microsoft explicitly warns conversions can fail mid-flight.
6. Distribution management
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/--importis your portable backup and the way to move a distro off the system drive.--unregisterpermanently 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:
# %UserProfile%\.wslconfig
[experimental]
sparseVhd=trueNever 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 are | Path from Linux | Path from Windows | Fast for |
|---|---|---|---|
| Linux filesystem (ext4 VHDX) | /home/you/code | \\wsl.localhost\Ubuntu\home\you\code | Linux tools ✅ |
| Windows filesystem | /mnt/c/Users/you/code | C:\Users\you\code | Windows tools ✅ |
✅ Keep your repos in the Linux filesystem (
/home/you/...) when you work with Linux tooling (git, npm, build chains). Putting them on/mnt/cforces every file op across the OS boundary and is dramatically slower.\\wsl$still works as an alias for\\wsl.localhost.
# 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$)# Clone into the Linux fs (fast), NOT /mnt/c (slow)
mkdir -p ~/code && cd ~/code
git clone https://github.com/codeAmani-Solutions/your-repo.gitWindows 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):
# /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.tsandfoo.tscollisions 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\nand break shell scripts (bad interpreter: /bin/bash^M). Setgit config --global core.autocrlf inputinside 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
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-findbuild-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:
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 -vWith nvm you never sudo npm install -g again — globals land in ~/.nvm, owned by you. Per-project switching works off .nvmrc:
echo "lts/*" > .nvmrc
nvm use # honours .nvmrc in the current repo
corepack enable # pnpm / yarn shims, shipped with NodeAlternatives:
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
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 trueReuse 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:
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:
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 login8.4 Verify the stack end to end
mkdir -p ~/code && cd ~/code
npx create-next-app@latest smoke-test --ts --app --tailwind --eslint
cd smoke-test && npm run devOpen 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 andtsc --watchgo 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):
| Command | Does |
|---|---|
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 --online | List installable distros |
wsl -l -v | List installed distros, version, state |
wsl -l --running / -l --quiet | Only running distros / names only |
wsl --set-version <Distro> <1|2> | Convert a distro's WSL version |
wsl --set-default-version 2 | Default 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 --status | WSL/kernel/WSLg versions · default distro + version |
wsl --shutdown | Stop 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> / --unmount | Attach/detach a physical or virtual disk |
wsl hostname -I | WSL 2 VM IP address |
<Distro> config --default-user <User> | Default login user (launcher distros only) |
wslconfig.exe,bash.exe, andlxrun.exeare the deprecated originals. Everything iswsl/wsl.exenow.
10. Configuration: .wslconfig vs wsl.conf
Two files, two scopes:
| Aspect | .wslconfig | wsl.conf |
|---|---|---|
| Scope | Global — the WSL 2 VM, all distros | Per-distro (works on WSL 1 too) |
| Location | %UserProfile%\.wslconfig (Windows) | /etc/wsl.conf (inside the distro) |
| Configures | RAM, CPUs, swap, networking, kernel, VHD size | boot/systemd, automount, network, interop, default user, GPU, timezone |
| Applied by | wsl --shutdown, then relaunch | Distro restart (wsl --terminate <Distro>) |
# %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⚠️
autoMemoryReclaimandsparseVhdlive under[experimental], not[wsl2]. Put them in the wrong section and WSL silently ignores them — a config that looks right and does nothing.
# /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 timezoneThe 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:
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):
sudo nano /etc/wsl.conf[boot]
systemd=truewsl --shutdown # from Windows, then relaunch the distroOnce systemd is PID 1, Ubuntu behaves like a normal server — which is exactly what you want for local Postgres, Redis, or an SSH daemon:
sudo systemctl enable --now postgresql
systemctl status postgresql
journalctl -u postgresql -n 50 --no-pagerServices enabled with
systemctl enablestart 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 -- truefrom 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
# 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# Run Linux commands from Windows
wsl ls -la ~
wsl --cd ~ -- bash -lc "npm run build"
wsl grep -r "TODO" .
dir | wsl grep gitWindows 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.
# 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). TurningappendWindowsPath=falseoff speeds up shell startup and tab completion noticeably, at the cost of losing barecode/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.
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# %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 themNetworking 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
dockerCLI 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/...:/appmount reintroduces the whole/mnt/cpenalty 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-winproject (usbipd attach --wsl). Serial ports remain WSL 1 only. - GUI apps (WSLg) — Linux GUI apps run out of the box (
guiApplications=true); justsudo apt installand launch. Handy forgedit, GUI database clients, or a Linux Chrome for testing.
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.
# 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)# ~/.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:
cd ~/code/your-repo
code . # opens the editor as a client; the server runs inside WSLThe 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. Usecode .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 likecode ., 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.
# 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 projectWhy 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, andnpmrun 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 -gwrites to~/.nvm, so nosudo, 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.exefrom a Windows shell works, but two things bite. Do not pass an env block (Windows env vars leak throughWSLENVand mangle the Linux environment), and setMSYS_NO_PATHCONV=1when invoking from Git Bash or it rewrites/home/you/...into a Windows path. Simplest is to run the agent inside the distro.
Run
wsl --updateperiodically 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.
# 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 scratchHarden the throwaway before running anything you don't trust:
# /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 unreachableThat 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
# 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"# Windows-side: a scheduled nightly WSL backup (Task Scheduler / PowerShell)
wsl --shutdown
wsl --export Ubuntu "D:\backups\ubuntu-$(Get-Date -Format yyyyMMdd).tar"// .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
| Issue | Fix |
|---|---|
wsl --install only prints help | WSL already installed — use wsl --install -d <Distro> |
| Install hangs at 0.0% | wsl --install --web-download -d <Distro> |
| Microsoft Store blocked by policy | wsl --install --web-download, or download a .wsl image and wsl --install --from-file |
| Everything feels slow | Repo is on /mnt/c — move it to ~/ (the Linux fs) |
| File watching / Fast Refresh misses saves | Same cause: inotify doesn't cross the /mnt/c boundary. Move the repo to ~/ |
| Forgot the Ubuntu password | wsl -d Ubuntu -u root, then passwd <username> |
.wslconfig change ignored | Run wsl --shutdown, then relaunch. Confirm with wsl -l --running |
autoMemoryReclaim / sparseVhd ignored | They belong under [experimental], not [wsl2] |
wsl.conf change ignored | The 8-second rule — wsl --terminate <Distro> before relaunching |
| systemd not running | Add [boot] systemd=true to /etc/wsl.conf, wsl --shutdown; needs a Store-serviced WSL |
chmod on /mnt/c does nothing | DrvFs has metadata off by default — add options="metadata,..." to [automount] |
bad interpreter: /bin/bash^M | CRLF line endings — git config --global core.autocrlf input, re-checkout |
localhost:3000 unreachable from Windows | Confirm the server binds 0.0.0.0/localhost; try networkingMode=mirrored |
| VM eating RAM | Cap it: [wsl2] memory=8GB + [experimental] autoMemoryReclaim=gradual |
| Disk keeps growing after deleting files | The 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/USB | Install the vendor WSL driver on Windows; for USB use usbipd-win |
| Clock drift after the host sleeps | sudo hwclock -s, or keep [time] useWindowsTimezone=true and systemd-timesyncd on |
| Distro corrupted | Recover 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 defaultUbuntuunless a project's container base image says otherwise, in which case install the matchingUbuntu-XX.04and 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.claudehook 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 thesudo npm install -ghabit, which is how root-owned files end up in~. Commit an.nvmrcto each repo sonvm useis 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 onwsl.exe, andMSYS_NO_PATHCONV=1under 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.localon 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=gradualso the WSL VM doesn't starve Windows. AddsparseVhd=truebefore you create distros, not after — it only applies to new VHDs. - Back up before anything destructive.
wsl --exportis fast and the tar is portable.wsl --unregisterandwsl --set-versionare 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.
Related guides
| Guide | Why it pairs with WSL |
|---|---|
| vscode | Remote-WSL setup, per-side extensions, debugging into the distro |
| cursor | Same Remote-WSL split, AI indexing the Linux-side workspace |
| local-database | Postgres/Redis in WSL — systemd service vs Docker container |
| sandbox | Disposable distros and stronger isolation patterns |
| chrome-devtools | Verifying a WSL-hosted dev server in a real Windows browser |