🏖️

WSL をサンドボックス環境にする

に公開
2025/12/22

発端

コーディングエージェントの CLI ツールでサンドボックスを使う場合、Windows は不遇職です。

他の環境がコマンドラインオプションでサクッと実現できることを、Windows では Docker を使ってアレコレすることになります。

多くの場合、公式が出来上がったコンテナイメージを公開しているんですが、基本的に C# プロジェクトは対象になっていないことが多いので自作する必要があります。

最近まで C# 絡みで馬鹿正直にやってたんですが、CLI ツールの更新頻度が高く、毎回そこそこ時間のかかるコンテナイメージの作り直しが面倒になりました。

断念)Docker を逐次更新

MCP 絡みで uv をインストール等々、何かが必要になる度にイメージの設定を更新して作り直すのが面倒なので、Docker 上の作業を毎回コミットする形にすれば良いんじゃないか? と思い試してみました。

が、とにかくターミナルが微妙過ぎて使い物にならず。。。(色が付かない/矢印キーで ANSI シーケンスがそのまま入力される等)他にもインストールそのもの+コミットによる永続化で2倍時間がかかる等、うーーんなことが多かったです。

バッチコマンド

なんか上手いやり方ありそうですけどね?

@echo off

:: 変数設定

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


:: 古いコンテナが残っていれば削除

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


:: インタラクティブにコンテナを起動(bashシェルを開く)

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


:: コンテナ内で作業が終了したら、コンテナの変更を新しいイメージとしてコミット

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


:: コンテナを削除

docker rm %CONTAINER_NAME%  || call :ERROR


:: コミット成功のメッセージ

echo コンテナの変更を "%NEW_IMAGE_NAME%" としてコミットしました。


exit /b

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

Docker を使った運用はかなり厳しく、AI の出力する DIFF 表示がモノクロで体験が終わってます。

WSL をサンドボックス化する

WSL はほぼコーディングエージェントの CLI ツールを使うだけの環境になっていることも多いと思います。そして WSL は Docker for Windows の基盤としても使われています。

それならば、Docker を使わずに WSL そのものをサンドボックス環境にしてしまえば良いだろうと。

それが 👇 コチラ。

概要

  • WSL 環境内はやりたい放題できる(rm -rf / も可能)
  • ただし、WSL 内からホスト側のドライブへのアクセスは遮断
  • バッチを起動したフォルダーは ~/<起動フォルダー名> として「読み書き可」でマウント
    • なので AI が rm -rf / を実行するとデータ消失(しょうがない)
  • npm i -g ... 等々、WSL 上で行った作業は全て永続化される
    • 何かするたびにイメージを作り直さなくて良い

起動スクリプト

  • Powershell スクリプトですが .bat ファイルとして保存して実行します。
  • 改行コードは CRLF が安定
sandbox.bat
@echo off

:: ========================================================
:: Polyglot Script
:: BAT 部分で PowerShell を呼び出す
:: 
::     【注】改行コードは必ず「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 版: WSL マウントスクリプト
# ========================================================


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


# カレントディレクトリを取得

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


# カレントディレクトリ名だけを抽出

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

# 空白をチェック(絶対に空白を含むフォルダーに対応できないから修正するな!!)

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


# Windows パスの \ を / に変換

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


# ドライブ文字、ユーザーパス、WSL上のマウント先を計算

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

# ~ は使えない(使えるが使うな!) ※ $HOME はエスケープして $FOLDERNAME は展開

$MNT_WINPWD = "`$HOME/$FOLDERNAME"


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


# ========================================================
# /etc/wsl.conf の automount 設定をチェック
# ========================================================

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

# そのままファイルに書き込まれるから [automount] 前の改行消すな! インデント弄るな!

$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
"@


# 読み取り専用でマウントしたい場合は以下
# [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"
}


# ========================================================
# WSL 起動: Bash に変数を渡してマウント
# ========================================================

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

$wslScript = @"
#
#   クォート弄るな! 統一されてないんじゃなくて全て意図的に変えてる!!
#


# 全ての automount ディレクトリを対象にアンマウント処理(\`$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


### Windows プロファイルを 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


### カレントディレクトリをマウント
#
#   $MNT_WINPWD をクォートで囲うな! 出来るハズと思い込むのはポリグロットでは問題になる! やめろ!!
#

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


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

ちゃんと色も付くし、上矢印キーでコマンドの履歴も出ます。(当然)

素晴らしい!

詳細

バッチを起動すると、まずは WSL 上の /etc/wsl.conf の自動更新を試みます。

[automount] が見つからなかった場合のみ

# 以下を追加

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

[interop]
appendWindowsPath=false

その後、Windows 側の「バッチの起動フォルダー(つまり cwd/pwd)」を WSL にマウントして bash -l を起動します。

その他 WSL 絡みの諸々

WSL 環境の確認

WSL のサンドボックス環境の確認方法(WSL にはバージョン1と2がある)

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

wsl --version
wsl --list --verbose

ホスト側のフォルダーを追加でマウントする

AI エージェント向けのドットフォルダー等を起動時に自動でマウントするように構成。

sudo nano /etc/fstab でエディターを起動

  • metadata は Linux 側の権限(chmod)を上手いことハンドリングするオプション
  • ro はそのまま readonly
# Windows のフォルダを WSL 内に読み取り専用でマウントする設定
# (スペースは \040 でエスケープ)

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

設定の確認方法

fstab の構文チェック(WSL)

sudo mount -a

問題がなかったら WSL から exit して、CMD/Powershell 上で

wsl --shutdown

を実行して、全 WSL インスタンスを再起動。

再起動後に上記のスクリプトを起動して WSL に戻ったら、

findmnt

で、マウントされているドライブの ro や rw 等々を確認。

おわりに

Docker 等々を使うのが正攻法だと思いますが、WSL(Hyper-V?)は Docker for Windows でも使われている技術です。

なので(手抜きっちゃ手抜きなんですが)なんちゃってサンドボックス環境という訳ではなく「面倒だしえいっとやっちゃえ!」よりは遥かに信頼性の高い状態で AI エージェントをオートパイロットできるんじゃないでしょうか。

以上です。お疲れ様でした。

Discussion