← Back to dashboard
sandboxtoolingfreshReader view (for NotebookLM)

WSL Ubuntu Sandbox Guide

What is a WSL sandbox?

The real model

Container-grade isolation with VM-grade convenience — and you have to know which one you actually got.

Microsoft's own architecture note is the line to internalise: WSL 2 distros share the network namespace, device tree, CPU/kernel/memory/swap and the /init binary, and only get their own PID, mount, user and cgroup namespaces. So a second distro is a container inside one shared utility VM, not a second VM — a kernel exploit crosses the distro boundary, port :3000 collides with your dev distro, and /mnt/wsl is a tmpfs every distro can see. The hard wall is Hyper-V, between the VM and Windows. .wslconfig is VM-global, so there is no per-distro RAM cap; cap the workload with systemd-run --scope -p MemoryMax=2G, or drop to docker run --rm --network none --memory 2g, which is the only tier that actually gives you a private network namespace. The default that bites hardest is automount: a Linux binary touching /mnt/c does so with the permissions of the Windows user who ran wsl.exe, so an unhardened sandbox can read every .env.local and SSH key in your profile. For codeAmani this is where the security guide's isolated-lab requirement lands, where an unfamiliar dependency gets its install script watched with the network off before it enters the libraries catalog, and where a customer repro gets built from a known-clean base instead of from whatever your laptop happens to be today.

Six moves that make a sandbox

One wsl.exe binary, no SDK — the discipline is in which verb you reach for and in what order.

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

WSL Ubuntu Sandbox Guide

Focus: Standing up a disposable, isolated Ubuntu on Windows for experiments — spin-up, snapshot, blast-radius containment, and a one-command nuke — so a risky install or a customer repro never lands in your real dev distro.

Overview

The wsl/ guide covers WSL as your primary dev environment. This one covers the opposite posture: a distro you expect to break. The whole workflow is four verbs on the same wsl.exe binary you already have — no extra tooling, no VM images to download.

VerbCommandCost
Spin upwsl --install Ubuntu-24.04 --name lab~1 min, one download
Snapshotwsl --export lab lab-clean.tar.gz --format tar.gzseconds–minutes, one file
Restorewsl --import lab-2 D:\wsl\lab-2 lab-clean.tar.gzseconds
Nukewsl --unregister labinstant, unrecoverable

The important thing to internalise before you trust it with anything dangerous: all WSL 2 distros run inside one shared lightweight utility VM. Per Microsoft's own architecture note, distros "share the same network namespace, device tree (other than /dev/pts), CPU/Kernel/Memory/Swap, /init binary, but have their own PID namespace, Mount namespace, User namespace, Cgroup namespace, and init process." The hard wall is the Hyper-V boundary between the VM and Windows — not the boundary between two distros.

Which tier do you actually need?

TierWhat you getReset costReach for it when
Second WSL distroOwn ext4 VHDX + mount/PID/user/cgroup namespaceswsl --unregister — instantA risky curl | bash, a whole toolchain, a repro that needs systemd
Snapshot + importThe above, plus a known-good starting pointRe-import a tarball — under a minuteYou need to run the same experiment five times from a clean base
Docker containerEverything a distro gets plus its own network namespace and hard cgroup capsAutomatic on --rmOne process, one command, no persistent state
Full Hyper-V VMSeparate kernel, separate network stack, checkpointsRestore checkpoint — minutesDetonating actual malware, or testing kernel modules

Official Documentation

ResourceURL
Basic WSL commands (export / import / unregister / manage)https://learn.microsoft.com/windows/wsl/basic-commands
Advanced settings — .wslconfig and wsl.confhttps://learn.microsoft.com/windows/wsl/wsl-config
WSL 2 architecture (what's shared, what isn't)https://learn.microsoft.com/windows/wsl/about
Enterprise setup — images, firewall, file-access modelhttps://learn.microsoft.com/windows/wsl/enterprise
Import a custom distro from a tarballhttps://learn.microsoft.com/windows/wsl/use-custom-distro
Manage disk space / sparse VHDXhttps://learn.microsoft.com/windows/wsl/disk-space
Hyper-V Firewall (filtering WSL traffic)https://learn.microsoft.com/windows/security/operating-system-security/network-security/windows-firewall/hyper-v-firewall
WSL releases (check what your build supports)https://github.com/microsoft/WSL/releases
docker run referencehttps://docs.docker.com/reference/cli/docker/container/run/
Docker none network driverhttps://docs.docker.com/engine/network/drivers/none/

Quickstart — a throwaway Ubuntu in 60 seconds

Bash
# Stand up a SECOND Ubuntu named "lab" — --name is what makes a second
# copy of an already-installed distro possible.
wsl.exe --install Ubuntu-24.04 --name lab --no-launch

# Enter it
wsl.exe -d lab

# ...break things...

# Nuke it. Instant. Unrecoverable. That's the point.
wsl.exe --unregister lab

From PowerShell, drop the .exe. Everything below uses wsl (PowerShell form) except where a Linux-side command is being run.

Confirm what's installed and which VM version each distro is on:

Bash
wsl.exe --list --verbose
wsl.exe --version          # verify your WSL build supports the flags below
wsl.exe --list --online    # real, current distro names for --install

--list --online today includes Ubuntu-26.04, Ubuntu-24.04, Ubuntu-22.04, Debian, kali-linux, archlinux, FedoraLinux-44, and the AlmaLinux / openSUSE / SUSE / Oracle families. Use the NAME column verbatim.

Placing and sizing the sandbox

Put the sandbox VHDX somewhere you don't mind filling up, and cap it:

PowerShell
wsl --install Ubuntu-24.04 `
  --name lab `
  --location D:\wsl\lab `
  --vhd-size 40GB `
  --no-launch
Install flagEffect
--name <Name>Registers under a custom name — the key to running N copies of one distro
--location <Path>Where the ext4.vhdx lives (default is under %LocalAppData%\wsl)
--vhd-size <Size>Caps the virtual disk (default defaultVhdSize is 1 TB)
--no-launch, -nRegister without launching — you configure wsl.conf before first boot
--fixed-vhdFixed-size rather than dynamically expanding disk
--from-file <Path>Install from a local distro file instead of the store
--version <1|2>Force WSL 1 or WSL 2 for this distro

Snapshot and restore

This is the part that turns a second distro into a sandbox. Get the environment to a known-good state, export it, and from then on every experiment starts from that file.

Take the golden snapshot

PowerShell
# Terminate first so the filesystem is quiescent
wsl --terminate lab

# tar is the portable format; tar.gz / tar.xz trade CPU for size
wsl --export lab D:\wsl\snapshots\lab-clean.tar.gz --format tar.gz

--format accepts tar, tar.gz, tar.xz, and vhd. The older --vhd switch is still accepted and is equivalent to --format vhd. A .vhdx export restores fastest (no untar) but is far larger and only meaningful for WSL 2.

<FileName> can be - for stdout (and - for stdin on import), so an export can be streamed into another tool. Do this from cmd or a POSIX shell, not PowerShell — PowerShell's object pipeline mangles binary streams:

Bash
# Git Bash / WSL — stream a snapshot straight to a compressor or a remote host
WSL_UTF8=1 wsl.exe --export lab - | zstd -T0 -19 -o /d/wsl/snapshots/lab-clean.tar.zst

Restore into a fresh distro

PowerShell
wsl --import lab-2 D:\wsl\lab-2 D:\wsl\snapshots\lab-clean.tar.gz --version 2
wsl -d lab-2
Import flagEffect
--version <1|2>WSL version for the new distro
--vhdThe source file is a .vhdx, not a tar — WSL copies it to <InstallLocation>

For a .vhdx you want to attach without copying (fast, but the file becomes the live disk):

PowerShell
wsl --import-in-place lab-3 D:\wsl\images\lab.vhdx

The VHD must be formatted ext4.

The reset loop

PowerShell
# One experiment, start to finish
wsl --unregister lab
wsl --import lab D:\wsl\lab D:\wsl\snapshots\lab-clean.tar.gz --version 2
wsl -d lab -- bash -lc './experiment.sh'

Because --unregister deletes the root filesystem outright, this loop is genuinely a few seconds. Treat lab-clean.tar.gz as immutable and never export over it from a dirty distro.

Post-import housekeeping

An imported distro has no launcher executable, which breaks the usual ubuntu config --default-user trick. Use wsl --manage instead:

PowerShell
wsl --manage lab --set-default-user amani     # imported distros boot as root otherwise
wsl --manage lab --set-sparse true            # auto-reclaim freed disk space
wsl --manage lab --resize 60GB                # grow (or shrink) the VHDX
wsl --manage lab --move E:\wsl\lab            # relocate the distro's disk

Or set it inside the distro before you snapshot, in /etc/wsl.conf:

INI
[user]
default=amani

Containing the blast radius

A fresh distro is not contained by default: it automounts your Windows drives and can launch Windows binaries as you. Harden it in /etc/wsl.conf before you take the golden snapshot.

Bash
# Inside the sandbox distro
sudo tee /etc/wsl.conf >/dev/null <<'EOF'
# Do not mount C:\ (and friends) into the sandbox at all.
[automount]
enabled = false
mountFsTab = false

# Do not let Linux processes launch Windows binaries, and keep the
# Windows PATH out of $PATH.
[interop]
enabled = false
appendWindowsPath = false

[network]
hostname = lab
generateResolvConf = true

[user]
default = amani

[boot]
systemd = true
EOF

Then, from Windows:

PowerShell
wsl --terminate lab   # settings only apply after the distro fully stops

What each switch buys you:

SettingWithout itWith it
[automount] enabled=false/mnt/c exposes your entire Windows profile — SSH keys, .env.local, browser profiles — writable as your Windows userThe sandbox cannot see Windows files at all
[interop] enabled=falseA script in the sandbox can run powershell.exe, explorer.exe, or any Windows binary as youWindows process launch is blocked
[interop] appendWindowsPath=falseWindows PATH entries leak into $PATH; a typo can silently invoke a Windows toolClean Linux-only PATH
[boot] systemd=trueNo service manager — many repros won't reproduceReal systemctl, matching production

Microsoft's enterprise guidance is explicit about why automount matters: when a Linux binary in WSL touches a Windows file, it does so with the permissions of the Windows user who ran wsl.exe. Root inside the sandbox is not root on Windows — but it is you on Windows, for every file you can reach.

What you still don't get

Be honest with yourself about the boundary, especially before running anything you'd call malware:

  • Shared kernel. Every distro runs on the one microsoft-standard-WSL2 kernel. A kernel-level exploit escapes the distro boundary into the VM. It does not escape the Hyper-V boundary into Windows — that's the wall that counts.
  • Shared network namespace. Distros share it. A listener bound in the sandbox collides with your dev distro's :3000, and a process in the sandbox can reach services your dev distro is serving on localhost.
  • Shared /mnt/wsl. A tmpfs visible from every running distro. Anything written there is cross-distro.
  • .wslconfig is VM-global. There is no per-distro memory or CPU cap. memory=8GB caps the entire VM shared by dev and sandbox.
  • wsl --debug-shell opens a root shell in the utility VM itself. It's a diagnostic tool; it's also a reminder of where the real boundary sits.
  • Docker Desktop registers its own docker-desktop distro in the same VM.

For anything genuinely hostile: a full Hyper-V VM (or Windows Sandbox, or a disposable cloud box) — not a WSL distro.


Resource limits with .wslconfig

.wslconfig lives on the Windows side at %UserProfile%\.wslconfig and governs the whole WSL 2 VM. Keeping a runaway make -j$(nproc) in the sandbox from freezing Windows is exactly what it's for.

INI
# %UserProfile%\.wslconfig   —  applies to the VM shared by ALL WSL 2 distros
[wsl2]
memory=8GB              # default: 50% of host RAM
processors=4            # default: all logical processors
swap=4GB                # default: 25% of memory, rounded up to the nearest GB
swapFile=D:\\wsl\\swap.vhdx
defaultVhdSize=64GB     # cap new distro disks (default 1 TB)
vmIdleTimeout=60000     # ms of idle before the VM shuts down (Windows 11)
nestedVirtualization=true
localhostForwarding=true

[experimental]
autoMemoryReclaim=gradual   # reclaim cache slowly instead of dropCache
sparseVhd=true              # every new VHDX is sparse — disk comes back on delete

Notes that will save you an hour:

  • Paths need escaped backslashes (D:\\wsl\\swap.vhdx). Sizes take GB/MB suffixes; bare numbers are bytes.
  • Nothing applies until the VM restarts. Run wsl --shutdown — a --terminate of one distro is not enough. Give it ~8 seconds before relaunching.
  • A malformed file is silently ignored. WSL boots with defaults and tells you nothing. Verify with free -h and nproc inside a distro after the shutdown.
  • Windows 11 ships a WSL Settings GUI app that edits this file; it's the recommended path over hand-editing.

Per-experiment limits

Since .wslconfig can't cap a single distro, cap the workload instead — systemd-run inside the sandbox:

Bash
# Needs [boot] systemd=true in the sandbox's wsl.conf
sudo systemd-run --scope \
  -p MemoryMax=2G -p CPUQuota=200% -p TasksMax=512 \
  ./experiment.sh

(--user scopes work too, but only where systemd has delegated the memory and pids controllers to the user slice — sudo is the version that always applies.)

...or run the experiment in a container, which is the next section.


Networking isolation

WSL's network knobs are all in [wsl2], and all global:

INI
[wsl2]
networkingMode=mirrored   # nat (default) | mirrored | virtioproxy | none
firewall=true             # Windows Firewall + Hyper-V rules filter WSL traffic (default)
dnsTunneling=true         # DNS via virtualization rather than packets — VPN-friendly
autoProxy=true            # inherit the Windows HTTP proxy
  • networkingMode=none disconnects WSL networking entirely — a genuine air-gap, but for every distro at once. Practical as a deliberate "offline experiment" mode: set it, wsl --shutdown, run the experiment, revert.
  • firewall=true (the default since WSL 2.0.9 on Windows 11 22H2+) means your Windows Firewall rules already apply to WSL. Per-distro rules are possible via Hyper-V Firewall.
  • For per-sandbox network isolation that doesn't disturb your dev distro, don't fight .wslconfig — use a container with --network none, or iptables/nftables inside the sandbox distro itself.

Docker containers — the lighter sandbox

When the experiment is one process rather than a whole environment, a container is faster, tighter, and self-cleaning. Run this from inside your normal distro (or Docker Desktop's WSL integration):

Bash
# Ephemeral, network-isolated Ubuntu shell. Gone the moment you exit.
docker run --rm -it --network none ubuntu:24.04 bash

A hardened version for running something you actively distrust:

Bash
docker run --rm -it \
  --network none \
  --memory 2g --cpus 2 --pids-limit 512 \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --read-only --tmpfs /tmp:rw,noexec,nosuid,size=256m \
  --user 1000:1000 \
  -v "$PWD/artifacts:/out:rw" \
  -w /out \
  ubuntu:24.04 bash
FlagWhy it's there
--rmContainer and its writable layer are removed on exit — no cleanup discipline required
--network noneThe container gets a loopback-only namespace. This is the isolation a second WSL distro can't give you.
--memory / --cpus / --pids-limitReal cgroup caps — a fork bomb hits the limit, not your laptop
--cap-drop ALL + --security-opt no-new-privilegesNo CAP_*, no setuid escalation
--read-only + --tmpfs /tmpImmutable root; scratch space that can't execute
--user 1000:1000Not root. Without user-namespace remapping, container root maps to host root on the shared kernel
-v ...:/outOne narrow, explicit path for results to escape through

Container vs. distro, decided in one line: if you need systemctl, a persistent home directory, or a multi-day environment, use a distro. If you need "run this and forget it", use a container.


The nuke-and-rebuild script

Keep this next to the golden snapshot. It's the whole workflow in one file.

PowerShell
# D:\wsl\reset-lab.ps1
param(
  # No default for $Name: --unregister has no undo, so the caller must say it out loud.
  [Parameter(Mandatory)][string]$Name,
  [string]$Root     = "D:\wsl\lab",
  [string]$Snapshot = "D:\wsl\snapshots\lab-clean.tar.gz"
)

$env:WSL_UTF8 = 1   # otherwise wsl.exe output is UTF-16 and every match fails

if (-not (Test-Path $Snapshot)) { throw "No snapshot at $Snapshot" }
if ($Name -eq "Ubuntu")         { throw "Refusing to nuke the default distro" }

# Only tear down if it is actually registered.
if ((wsl --list --quiet) -contains $Name) {
  wsl --terminate  $Name
  wsl --unregister $Name
  if ($LASTEXITCODE -ne 0) { throw "unregister failed for $Name" }
}

New-Item -ItemType Directory -Force -Path $Root | Out-Null
wsl --import $Name $Root $Snapshot --version 2
if ($LASTEXITCODE -ne 0) { throw "import failed from $Snapshot" }

wsl --manage $Name --set-sparse true

Write-Host "Sandbox '$Name' rebuilt from $Snapshot"
wsl --list --verbose

And the one-way door, spelled out because it deserves it:

PowerShell
wsl --unregister lab

--unregister deletes the distro's root filesystem. There is no recycle bin, no undo, and no prompt. Type the distro name carefully — wsl --unregister Ubuntu and wsl --unregister lab are one character apart in muscle memory.


Scripting wsl.exe from agents and CI

wsl.exe emits UTF-16LE with no BOM by default, which turns into interleaved NUL bytes in any pipeline that assumes UTF-8 — the classic "why is my output U b u n t u". Set WSL_UTF8=1:

Bash
WSL_UTF8=1 wsl.exe --list --verbose
PowerShell
$env:WSL_UTF8 = 1
wsl --list --verbose

Two more scripting notes:

  • From Git Bash / MSYS, Windows paths in wsl.exe arguments get path-mangled. Prefix with MSYS_NO_PATHCONV=1, or run those commands from PowerShell.
  • wsl.exe returns -1 (255) on failure with an Wsl/Service/... error code on stderr. Check the exit code; don't parse the message.

codeAmani notes

A sandbox is where risky things go, not where secrets go.

  • Never seed a sandbox distro with real credentials. Hazina bindings, .env.local, ~/.ssh, ~/.aws, and cloud CLI tokens stay in the dev distro. With [automount] enabled=false and [interop] enabled=false in the sandbox's wsl.conf, they're not reachable from it — which is the entire point. If an experiment needs a credential, mint a scoped test key for it and revoke it when the distro is unregistered.
  • Pairs with the security guide's isolated-lab requirement. Scanners, exploit PoCs, dependency-confusion checks, and gitleaks runs against untrusted repos belong here, one snapshot-restore per run so results are never contaminated by the previous one.
  • Customer-bug repros. Import the golden snapshot, install exactly the reported versions, reproduce, capture the diff, unregister. No "it works on my machine" because the machine was clean an hour ago.
  • Vetting a new dependency before it enters the catalog. libraries/ build-vs-buy calls often hinge on running an unfamiliar package's install script. Run it in lab with the network off (--network none container, or networkingMode=none for the run) and watch what it tries to do.
  • Supply-chain posture. The sandbox is where you observe; it doesn't substitute for the real controls. SLSA provenance, npm audit signatures, dependency review, and secret scanning still gate anything codeAmani ships (see supply-chain/CLAUDE_CODE_INTEGRATION.md).
  • Low-bandwidth reality for the Kenya-targeted builds. Snapshot-and-restore is the bandwidth win here: one Ubuntu-24.04 download, then every rebuild is a local tarball. Export as tar.xz if a teammate on a metered connection has to pull the golden image; keep --format tar for local speed. Testing a duka-order-bot or boda-dispatch webhook path against a throttled interface is also easiest in a sandbox — tc qdisc there can't wreck your working network stack.
  • Don't run production-adjacent work in a sandbox you plan to nuke. Anything worth keeping goes back to git before --unregister. The sandbox has no backup, by design.
  • wsl/CLAUDE_CODE_INTEGRATION.md — WSL as your primary dev environment: filesystem performance, /mnt/c rules, Claude Code inside WSL, networking modes, systemd. Read that one first; this guide assumes it.
  • security/CLAUDE_CODE_INTEGRATION.md — the isolated-lab requirement this guide satisfies.
  • supply-chain/CLAUDE_CODE_INTEGRATION.md — SLSA provenance for anything that leaves the lab.