Tart VM で今より安全な Claude Code の環境を整える
はじめに
以前の記事(Apple Container + Xcode MCP で Claude Code を動かす)では Apple Container + Xcode MCP を使って Claude Code を隔離環境で動かす構成を紹介しました。ただ記事の末尾でも触れたように、Simulator がホスト Mac 上で動く以上、VM による境界が実質的に機能しないという問題が残っていました。
この記事では、その問題を解決するために Tart macOS VM に Xcode + Simulator を丸ごと移し、さらに Softnet を fork して DNS フィルタリングを実装した記録をまとめます。

ネットワーク制御の選択肢(なぜ Softnet を fork するのか)
VM の隔離はできます。しかしネットワーク制御にも課題があります。
「ホスト側の DNS 設定を変えておけばいいのでは?」と思うかもしれません。しかし VM 内のコードは DNS 設定を無視して、Google の 8.8.8.8 などに直接問い合わせることができます。ホスト側の DNS 設定は VM 内のコードから見れば「あくまで推奨」にすぎず、無視しようと思えばできてしまいます。
Softnet は VM の「外側」で動くユーザースペースのパケットフィルターです。vmnet.framework を通じて VM の仮想 NIC を流れるすべてのパケットを見張るため、VM 内のコードがどれだけ権限を持っていてもバイパスできません。sudo で root になっても VM の中での話なので、VM の外側で動く Softnet には届きません。
ただしオリジナルの Softnet には特定ドメインだけ許可する機能がないため、Softnet を fork して DNS フィルタリングを追加しました。
Softnet を fork して DNS フィルタリングを追加
Softnet を fork したリポジトリ: kusumotoa/softnet
allowlist 以外のドメインへの問い合わせには「そのドメインは存在しません」というエラーを返します。通信をサイレントにドロップすると DNS タイムアウト(30 秒程度)まで待たされますが、エラーを返すことで macOS の DNS リゾルバのリトライ(2〜3 回)が終わり次第、エラーが数秒以内に届きます。
VM がアクセスできるのは allowlist のドメインのみです。内部的には DNS レスポンスから IP アドレスを学習し、allowlist のドメインに解決された IP アドレスのみ通信を通過させることで実現しています。
設定は YAML 形式のファイル(dns-allowlist.yaml)で管理し、SOFTNET_DNS_ALLOWLIST 環境変数でファイルパスを指定できます。
run-vm.sh でワンコマンド起動
GitHub トークンを VM に渡すために、起動スクリプト run-vm.sh を用意しました。
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VM_NAME="tahoe-xcode"
TOKEN_FILE="$SCRIPT_DIR/github-token.txt"
# gh auth token でトークン取得
gh auth token > "$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
# VM 終了時にトークンファイルを削除
trap 'rm -f "$TOKEN_FILE"' EXIT
SOFTNET_DNS_ALLOWLIST="$SCRIPT_DIR/dns-allowlist.yaml" \
tart run "$VM_NAME" \
--net-softnet \
--dir "$SCRIPT_DIR:ro"
SOFTNET_DNS_ALLOWLIST はホスト側の環境変数として Softnet に渡します(VM 内には渡りません)。--net-softnet で Softnet によるネットワーク制御を有効化し、--dir "$SCRIPT_DIR:ro" でスクリプトと同じディレクトリを VM に読み取り専用(:ro)でマウントします。トークンファイルは VM 終了後に自動削除されます。
DNS フィルタにより VM からの通信先は allowlist ドメインに限定されます。そのため、ホストの gh auth token をそのまま VM に渡しても、許可していないドメインに渡す心配はありません。
後述する Packer を使った VM ビルドをしておけば、VM にログインした時点で GitHub 認証とリポジトリのクローンが自動実行されます。手動操作は不要です。
DNS 許可リスト
dns-allowlist.yaml の内容は以下のとおりです。
allowed_domains:
- api.github.com
- github.com
- "*.githubusercontent.com"
- "*.apple.com"
- "*.cdn-apple.com"
- api.anthropic.com
- "*.anthropic.com"
- claude.ai
- "*.claude.ai"
- "*.claude.com"
- statsigapi.net
- accounts.google.com
- "*.google.com"
- registry.npmjs.org
- cocoapods.org
- swiftpackageindex.com
*.apple.com と *.cdn-apple.com は Xcode・Simulator・SwiftPM が利用します。accounts.google.com と *.google.com は Claude Code の OAuth ログインに必要です。github.com はリポジトリのクローン時(HTTPS/gh CLI)に、api.github.com はアプリが GitHub API を呼び出す際にそれぞれ必要です。
allowlist に含まれないドメインへの問い合わせは数秒以内にエラーが返るため、外部への意図しない通信を確実に遮断できます。
セットアップ手順
Dockerfile に相当する tart.pkr.hcl を使い、Packer で VM をビルドします。OS バージョンを更新したい場合も vm_base_name を書き換えて packer build を再実行するだけで、同じ環境を再現できます。
tart.pkr.hcl
packer {
required_plugins {
tart = {
source = "github.com/cirruslabs/tart"
version = ">= 1.0.0"
}
}
}
source "tart-cli" "tahoe-xcode" {
vm_base_name = "ghcr.io/cirruslabs/macos-tahoe-xcode:26.3"
vm_name = "tahoe-xcode"
ssh_username = "admin"
ssh_password = "admin"
headless = true
}
build {
sources = ["source.tart-cli.tahoe-xcode"]
# Claude Code のインストール(既にインストール済みの場合は最新版に更新)
provisioner "shell" {
environment_vars = ["PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"]
inline = [
"command -v node || brew install node",
"npm install -g @anthropic-ai/claude-code --force",
]
}
# 初回セットアップスクリプトを VM に配置
provisioner "file" {
source = "first-run.sh"
destination = "/Users/admin/first-run.sh"
}
provisioner "shell" {
inline = [
"chmod +x ~/first-run.sh",
"echo '[ -x ~/first-run.sh ] && ~/first-run.sh' >> ~/.zshrc",
]
}
}
first-run.sh(tart.pkr.hcl と同じディレクトリに配置)
#!/bin/bash
FLAG="$HOME/.first-run-done"
# 2回目以降はスキップ
[ -f "$FLAG" ] && exit 0
echo "=== 初回セットアップを開始します ==="
TOKEN_FILE="/Volumes/My Shared Files/github-token.txt"
if [ ! -f "$TOKEN_FILE" ]; then
echo "Error: run-vm.sh を使って VM を起動してください。"
exit 1
fi
cat "$TOKEN_FILE" | gh auth login --with-token
cd ~ && gh repo clone username/repo # 自分のリポジトリに変更する
touch "$FLAG"
echo "=== セットアップ完了 ==="
username/repo の部分はクローンしたいリポジトリに合わせて変更してください。
# 1. Softnet をビルド・インストール(SUID ビット付き)
git clone https://github.com/kusumotoa/softnet
cd softnet
cargo build --release
sudo cp target/release/softnet /usr/local/bin/softnet
sudo chown root:wheel /usr/local/bin/softnet
sudo chmod u+s /usr/local/bin/softnet # vmnet.framework の利用に root 権限が必要
# 2. Packer をインストール
brew install hashicorp/tap/packer
# 3. Packer プラグインをインストールし VM をビルド
# Claude Code のインストールと初回セットアップスクリプトが VM に仕込まれる
cd <作業ディレクトリ> # tart.pkr.hcl と first-run.sh を置いたディレクトリ
packer init tart.pkr.hcl
packer build tart.pkr.hcl
# 4. run-vm.sh を実行
./run-vm.sh
VM にログインすると first-run.sh が自動実行され、GitHub 認証とリポジトリのクローンが完了します。2 回目以降の起動では自動的にスキップされます。
動作確認
構築後に確認した結果をまとめます。
api.github.com への接続は正常に開けました。youtube.com へのアクセスは allowlist に含まれないためエラーが返り、接続できませんでした。Claude Code の OAuth ログインは accounts.google.com が allowlist にあるため成功しました。gh repo clone でリポジトリを取得でき、Xcode 26.3 でプロジェクトを開いて Simulator でアプリを実行できることも確認しました。
まとめ
Apple Container + Xcode MCP 構成では Simulator がホスト Mac 上で動くため、VM 境界が機能しませんでした。Tart macOS VM に Xcode + Simulator を含めることで、VM 外への意図しないアクセスを遮断できます。
Softnet を fork して追加した DNS フィルタリングは VM の外側で動作するため、VM 内からバイパスできません。allowlist を開発に必要なドメインに絞ることで、Claude Code が生成したコードによる意図しない外部通信を防げます。
DNS フィルタで通信先を制限した上で、ホストの GitHub トークンを VM に共有する構成も実現できました。
なお、CI を使っている場合は注意が必要です。GitHub Actions などの CI 環境では、生成されたコードがビルドやテスト実行を通じて外部と通信する場合があり、Tart VM のフィルタ制御外になります。CI スクリプトの内容を確認しながら、信頼できるワークフローのみを実行するよう心がけると、より安全に運用できます。