iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🏖️

Turning WSL into a Sandbox Environment

に公開

Origin

When using sandboxes with coding agent CLI tools, Windows is a second-class citizen.

What other environments can achieve instantly with command-line options, you end up doing with Docker on Windows.

In many cases, official container images are provided, but since C# projects are rarely included, you often have to build your own.

Until recently, I was doing things the hard way for C#-related work, but with the high frequency of CLI tool updates, rebuilding container images every time—which takes a significant amount of time—became a hassle.

Abandoned: Updating Docker Sequentially

Since it's tedious to update the image settings and rebuild every time something like uv for MCP is needed, I thought, "Why not just commit the work on Docker every time?"

However, the terminal experience was simply too poor to be usable... (no colors, arrow keys inputting ANSI sequences directly, etc.). Additionally, the process took twice as long due to the installation itself plus the time for persistence via commit. It was quite a letdown.

Batch Command

There might be a better way to do this, though.

@echo off

:: Variable settings

set IMAGE_NAME=ubuntu
set CONTAINER_NAME=my_ubuntu_container
set NEW_IMAGE_NAME=ubuntu:latest


:: Remove old container if it exists

docker rm -f %CONTAINER_NAME%  || call :ERROR


:: Start container interactively (open bash shell)

docker run --name %CONTAINER_NAME% -it %IMAGE_NAME% /bin/bash -c ^
  "if ! id -u newuser &>/dev/null; then useradd -m newuser && echo 'newuser:newpassword' | chpasswd && usermod -aG sudo newuser; fi && su - newuser" ^
  || call :ERROR


:: Once work inside the container is finished, commit changes as a new image

docker commit %CONTAINER_NAME% %NEW_IMAGE_NAME%  || call :ERROR


:: Remove container

docker rm %CONTAINER_NAME%  || call :ERROR


:: Commit success message

echo Committed container changes as "%NEW_IMAGE_NAME%".


exit /b

:ERROR
  echo.
  echo ======= ERROR =======
  echo.
  exit 310

Operating with Docker is quite harsh, and the AI's DIFF output being monochrome completely ruins the experience.

Sandboxing WSL

For many, the WSL environment might be used almost exclusively for running coding agent CLI tools. And WSL is also used as the foundation for Docker for Windows.

If that's the case, why not just turn WSL itself into a sandbox environment instead of using Docker?

And that's what this is 👇.

Overview

  • You can do whatever you want inside the WSL environment (even rm -rf / is fine).
  • However, access from within WSL to the host-side drives is blocked.
  • The folder from which the batch is launched is mounted as ~/<launch-folder-name> with "read-write" access.
    • Therefore, if the AI executes rm -rf /, data will be lost (this is unavoidable).
  • All work done on WSL, such as npm i -g ..., is persisted.
    • No need to rebuild images every time you make a change.

Startup Script

  • This is a PowerShell script, but save and run it as a .bat file.
  • CRLF line endings are more stable.
sandbox.bat
@echo off

:: ========================================================
:: Polyglot Script
:: Invoke PowerShell in the BAT section
:: 
::     [Note] Line endings must be "CRLF"!!
:: 
:: ========================================================

powershell -NoLogo -NoProfile -Command ^
  "$s = Get-Content '%~f0'; $i = $s.IndexOf('###__POWERSHELL__###'); Invoke-Expression ($s[($i+1)..($s.Length-1)] -join \"`n\")"

exit /b


###__POWERSHELL__###

# ========================================================
# PowerShell Version: WSL Mount Script
# ========================================================


#
#
#     MUST PLACE A BLANK LINE AFTER ALL JAPANESE COMMENTS
#
#     !!!!!!! IT'S WEIRD BUT ***CANNOT*** OMIT IT !!!!!!!
#
#


# Get current directory

$WINPWD = Get-Location
Write-Host "[INFO] Current PowerShell directory: $WINPWD"


# Extract only the current directory name

$FOLDERNAME = Split-Path $WINPWD -Leaf
Write-Host "[INFO] Folder name for WSL mount: $FOLDERNAME"

# Check for spaces (Do not fix this as it absolutely cannot handle folders with spaces!!)

if ($FOLDERNAME -match '\s') {
    Write-Host "[ERROR] Folder name contains spaces: '$FOLDERNAME'" -ForegroundColor Red
    exit 1
}


# Convert Windows path \ to /

$USERPROFILE_SLASH = $env:USERPROFILE -replace '\\','/'
$WINPWD_SLASH = $WINPWD.Path -replace '\\','/'
Write-Host "[INFO] USERPROFILE with / : $USERPROFILE_SLASH"
Write-Host "[INFO] Current dir with / : $WINPWD_SLASH"


# Calculate drive letter, user path, and mount destination on WSL

$DRIVELETTER = $USERPROFILE_SLASH.Substring(0,1).ToLower()
$USERPATH = $USERPROFILE_SLASH.Substring(2)
$MNT_WINHOME = "/mnt/$DRIVELETTER$USERPATH"

# Cannot use ~ (can be used, but don't!) * Escape $HOME and expand $FOLDERNAME

$MNT_WINPWD = "`$HOME/$FOLDERNAME"


Write-Host "[INFO] Mount paths:"
Write-Host "       UserProfile: $MNT_WINHOME"
Write-Host "      WorkspaceDir: $MNT_WINPWD"


# ========================================================
# Check automount settings in /etc/wsl.conf
# ========================================================

Write-Host "[INFO] Checking if [automount] section exists in /etc/wsl.conf..."

# Don't delete the newline before [automount]! Don't touch the indentation as it's written directly to the file!

$wslCheck = @"
echo ---
cat /etc/wsl.conf
echo ---
if ! grep -q '^\s*\[automount\]\s*$' /etc/wsl.conf 2>/dev/null; then
    sudo tee -a /etc/wsl.conf >/dev/null <<EOF

[automount]
enabled=false
mountFsTab=true
options=metadata,ro

[interop]
appendWindowsPath=false
EOF
    echo 'Added [automount] section. WSL needs restart...'
    exit 310
fi
"@


# Use the following to mount as read-only
# [automount]
# enabled=true
# mountFsTab=false
# options=metadata,ro


$wslCheck = $wslCheck -replace "`r",""
wsl bash -ec $wslCheck

if ($LASTEXITCODE -ne 0) {
    Write-Host "[INFO] Shutting down WSL to apply automount settings..."
    wsl --shutdown
    Write-Host ""
    Write-Host ""
    Write-Host "[INFO] Please run this script again to start WSL safely."
    Write-Host ""
    exit 1
} else {
    Write-Host "[INFO] Verified automount config"
}


# ========================================================
# Start WSL: Pass variables to Bash and mount
# ========================================================

Write-Host "[INFO] Starting WSL with isolated mount namespace..."

$wslScript = @"
#
#   Don't touch the quotes! They aren't inconsistent; they are all changed intentionally!!
#


# Unmount process for all automount directories (escaped with \$dir)

#for dir in /mnt/{a..z}; do
#    if [[ \`$dir != /mnt/c ]]; then
#        if mountpoint -q "\`$dir"; then
#            echo [INFO] Unmounting \`$dir
#            sudo umount --lazy "\`$dir"
#        fi
#    fi
#done


### Mount Windows profile as read-only

#if ! mountpoint -q '$MNT_WINHOME'; then
#    echo '[INFO] Mounting $USERPROFILE_SLASH to $MNT_WINHOME as read-only'
#    sudo mkdir -p '$MNT_WINHOME'
#    sudo mount -t drvfs '$USERPROFILE_SLASH' '$MNT_WINHOME' -o ro,metadata
#else
#    echo '[INFO] $MNT_WINHOME is already mounted'
#fi


### Mount the current directory
#
#   Don't wrap $MNT_WINPWD in quotes! Assuming it's possible will cause problems in polyglot scripts! Don't do it!!
#

if ! mountpoint -q $MNT_WINPWD; then
    echo '[INFO] Mounting $WINPWD_SLASH to $MNT_WINPWD'
    mkdir -p $MNT_WINPWD
    sudo mount -t drvfs '$WINPWD_SLASH' $MNT_WINPWD -o metadata
else
    echo '[INFO] $MNT_WINPWD is already mounted'
fi


### Start WSL

echo
echo '[TIP] How to mount windows folder'
echo '      * mkdir wsl/path'
echo '      * sudo mount -t drvfs Windows/Path wsl/path [-o [ro,]metadata]'
echo
cd $MNT_WINPWD
exec bash -l
"@


$wslScript = $wslScript -replace "`r",""
wsl bash -ec $wslScript

Colors work properly, and command history appears with the up arrow key. (As expected)

Wonderful!

Details

When you launch the batch, it first attempts to auto-update /etc/wsl.conf on WSL.

Only if [automount] is not found

# Add the following

[automount]
enabled=false
mountFsTab=true
options=metadata,ro

[interop]
appendWindowsPath=false

After that, it mounts the Windows-side "batch launch folder (i.e., cwd/pwd)" into WSL and starts bash -l.

Miscellaneous WSL-related Info

Checking the WSL Environment

How to check the WSL sandbox environment (WSL has versions 1 and 2):

wsl --install
wsl --update
wsl --set-default-version 2

wsl --version
wsl --list --verbose

Mounting Additional Host Folders

Configure dotfolders for AI agents, etc., to be automatically mounted at startup.

Open the editor with sudo nano /etc/fstab:

  • metadata is an option that handles Linux-side permissions (chmod) effectively.
  • ro stands for read-only.
# Settings to mount a Windows folder as read-only inside WSL
# (Spaces are escaped with \040)

# <file system>     <mount point>  <type> <options>            <dump> <pass>
C:/John\040Doe/Docs /mnt/documents drvfs  defaults,ro,metadata 0      0

How to Verify Settings

Syntax check for fstab (WSL):

sudo mount -a

If there are no issues, exit from WSL and run the following in CMD/PowerShell:

wsl --shutdown

to restart all WSL instances.

After restarting, launch the script mentioned above to return to WSL, then run:

findmnt

to check the ro, rw, etc., status of the mounted drives.

Conclusion

While using Docker and similar tools is likely the standard approach, WSL (Hyper-V?) is the technology that even Docker for Windows is built upon.

So (while it may be a bit of a shortcut), this isn't just some pseudo-sandbox environment. I believe it allows you to run AI agents on autopilot in a much more reliable state than simply winging it because it's a hassle.

That's all. Thank you.

Discussion