WSL — Linux on Windows Developer Guide

Technology: wsl · Category: tooling · Last reviewed: 2026-08-23

Source: https://tech-stack.codeamanilabs.org/guide/wsl

Insight:

WSL 2 runs a real Linux kernel in a lightweight managed VM beside Windows — 100% syscall compatibility, systemd, Docker, GPU. Ubuntu is the default distro and the one to use: wsl --install gives you the current Ubuntu LTS with systemd already on. The one rule that decides your whole experience: keep project files in the Linux filesystem (/home/you/code), never on /mnt/c. Cross-OS file access is the single thing WSL 1 does faster; storing code on the Linux side makes WSL 2 up to 20× faster on I/O-heavy work (git clone, npm install). It's the right place to run Claude Code on a Windows machine — a native Linux toolchain with Windows interop one explorer.exe . away.

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

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

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:

flowchart TB
  PS["PowerShell / Terminal"]
  EXP["File Explorer"]
  APP["Windows apps"]
  subgraph VM["Lightweight utility VM · Hyper-V"]
    K["Real Linux kernel + systemd"]
    U["Ubuntu container<br/>ext4.vhdx"]
    D2["Second distro<br/>own ext4.vhdx"]
    LFS["Linux fs: /home/you/code · FAST"]
    MNT["/mnt/c: Windows C drive · slow across boundary"]
    K --> U
    K --> D2
    U --> LFS
    U --> MNT
  end
  PS -->|wsl.exe interop| K
  EXP -->|"wsl.localhost share"| LFS
  APP -->|mount| MNT

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 --install

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

flowchart LR
  A["wsl --install"] --> B["Enable WSL + VM Platform"]
  B --> C["Download Linux kernel"]
  C --> D["Set WSL 2 as default"]
  D --> E["Install Ubuntu (latest LTS)"]
  E --> F["Reboot"]
  F --> G["Create Linux user + password"]
  G --> H["sudo apt update && sudo apt upgrade"]

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 versions

Useful --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 --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:

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 -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 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:

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:

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:

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:

wsl -d Ubuntu -u root        # root shell, no password needed
passwd 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=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:

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

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:

# %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:

flowchart LR
  LT["Linux tools: git, npm, builds"]
  WT["Windows tools: Explorer, editors"]
  LFS["Linux fs: /home/you/code"]
  MNT["Windows fs: /mnt/c"]
  LT -->|FAST| LFS
  WT -->|FAST| MNT
  LT -->|"2-20x SLOWER"| MNT
  WT -->|slow| LFS
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/c forces 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.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):

# /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:


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-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:

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:

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

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:

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 login

8.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 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):

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, and lxrun.exe are the deprecated originals. Everything is wsl/wsl.exe now.


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

⚠️ 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.

# /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:

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=true
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:

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

# 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 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.

# 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.

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 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 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:

# 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 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.

flowchart LR
  UI["Editor UI (Windows)<br/>rendering · keybindings"]
  SRV["Editor server (Ubuntu)<br/>~/.vscode-server"]
  EXT["Extensions · LSP · debugger"]
  FS["/home/you/code"]
  UI <-->|"Remote-WSL channel"| SRV
  SRV --> EXT
  EXT --> FS

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 project

Why it clicks:

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.

# 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:

# /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

# 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

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

Official docs: