Apple Container + Xcode MCP で Claude Code を隔離環境で動かす
はじめに
Claude Code の --dangerously-skip-permissions は、ツール実行時の確認プロンプトをすべてスキップするオプションです。ホスト macOS 上でそのまま実行するのはリスクが大きいため、Apple Container 内に隔離した環境を作ってみました。ただし Xcode MCP (mcpbridge) は macOS ネイティブのツールなので、SSH の stdio トンネルでコンテナからホストの mcpbridge を呼び出す構成にしています。
この記事ではセットアップ手順と、Apple Container 固有の制約に対する解決策をまとめています。
アーキテクチャ
コンテナ内の Claude Code からホスト macOS 上の Xcode に接続するために、SSH を使った stdio トンネルを構成しています。
Claude Code の MCP クライアントが SSH 経由でホスト macOS の SSH サーバーに接続すると、authorized_keys の command= 制約により mcp-dispatcher.sh が起動します。ディスパッチャは SSH_ORIGINAL_COMMAND を allowed-commands.txt のホワイトリストと照合し、許可されたコマンド(/usr/bin/xcrun mcpbridge)のみを実行します。stdin/stdout が SSH トンネルを通じて mcpbridge に接続され、JSON-RPC 2.0 メッセージがやり取りされます。mcpbridge は XPC を介して Xcode GUI プロセスと通信し、ビルドやテスト実行などの操作を実行します。
MCP の接続定義はプロジェクトルートの .mcp.json に記述してあり、volume mount でコンテナ内にそのまま共有されます。ホスト macOS ではネイティブの Xcode MCP が動くため、この定義と競合せずに共存できるのもポイントです。
前提条件
セットアップを始める前に、以下の環境が必要です。
- macOS 26 (Tahoe) 以降 + Apple silicon
- Xcode 26.3 以降(
xcrun mcpbridgeが利用可能であること) - Apple Container のインストール
Apple Container は GitHub Releases から container-installer-signed.pkg をダウンロードしてインストールできます。インストール後、システムサービスを開始しておきます。
container system start
セットアップ手順
1. macOS SSH サーバーの有効化
macOS のリモートログインを有効にします。システム設定を開き、「一般」>「共有」から「リモートログイン」をオンにしてください。許可するユーザーは、セキュリティの観点から自分のアカウントのみに制限しておくのがよいでしょう。
有効化したら、ターミナルで接続テストを実行します。
ssh localhost echo "SSH connection successful"
SSH connection successful と表示されれば OK です。
2. SSH ディスパッチャの構築
コンテナ内の Claude Code が SSH 経由でホスト上の任意のコマンドを実行できないよう、ホワイトリスト方式のディスパッチャを用意します。
この構成ではホワイトリストファイル(scripts/allowed-commands.txt)とディスパッチャスクリプト(scripts/mcp-dispatcher.sh)をリポジトリ内に配置し、authorized_keys の command= からリポジトリ内のスクリプトを直接参照する設計です。セットアップスクリプトを実行すると以下が自動的に行われます。
- macOS の Remote Login (sshd) が有効か確認(
nc -z localhost 22) - リポジトリ内の
scripts/mcp-dispatcher.shとscripts/allowed-commands.txtの存在確認 - コンテナ用の SSH 鍵ペア
container_ed25519を生成 authorized_keysにリポジトリ内のmcp-dispatcher.shの絶対パスをcommand=制約付きで登録
./scripts/setup-ssh-dispatcher.sh
新しいコマンドを許可したい場合は scripts/allowed-commands.txt に 1 行追加してセットアップスクリプトを再実行するだけです。authorized_keys を編集する必要はありません。
3. ホスト接続用 DNS の設定
Docker の host.docker.internal のように、コンテナからホストのサービスへ接続する手段は長らく要望されていました(Issue #346)。v0.9.0 で container system dns create に --localhost オプションが追加され、これが実現しています。
sudo container system dns create host.container.internal --localhost 203.0.113.113
このコマンドはホスト側の DNS エントリを作成し、203.0.113.113 宛のトラフィックを Packet Filter (pf) ルールで 127.0.0.1 にリダイレクトします。コンテナから host.container.internal に接続すると、ホストの localhost に到達できる仕組みです。
4. .mcp.json の配置
プロジェクトルートに .mcp.json を作成します。このファイルは volume mount でコンテナ内の /workspace にそのまま共有されます。
{
"mcpServers": {
"xcode": {
"command": "sh",
"args": [
"-c",
"ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes -o ConnectTimeout=5 \"${HOST_USER}@host.container.internal\" /usr/bin/xcrun mcpbridge"
]
}
}
}
sh -c で実行しているのは、コンテナ内で HOST_USER 環境変数を展開するためです。HOST_USER は起動スクリプトがコンテナに渡す環境変数で、ホストの macOS ユーザー名が入ります。
ホスト macOS ではネイティブの Xcode MCP が動いているため、host.container.internal への SSH 接続は失敗しますが、ネイティブ側の MCP が優先されるので問題ありません。
ワンコマンド起動スクリプト(自動化)
ここまでの手順を毎回手動で実行するのは面倒です。そこで、ホスト側から 1 コマンドですべてを済ませるスクリプトを用意しました。Dockerfile でイメージをビルドし、entrypoint.sh でランタイム初期化を行う構成になっています。
setup-ssh-dispatcher.sh(ホスト側の SSH 環境構築)
前述の SSH ディスパッチャをセットアップするスクリプトです。冪等に設計されているため、何度実行しても問題ありません。起動スクリプトから自動で呼び出されます。
#!/bin/bash
# scripts/setup-ssh-dispatcher.sh
# ホスト macOS で実行:SSH ディスパッチャ環境を構築する
# authorized_keys にリポジトリ内の mcp-dispatcher.sh を直接参照する command= 制約を登録する
# 冪等設計:何度実行しても安全
set -euo pipefail
# --- ヘルパー関数 ---
log_info() { echo "[INFO] $*"; }
log_error() { echo "[ERROR] $*" >&2; }
log_step() { echo ""; echo "==> $*"; }
# --- 定数 ---
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SSH_DIR="$HOME/.ssh"
AUTHORIZED_KEYS="$SSH_DIR/authorized_keys"
DISPATCHER_SCRIPT="$SCRIPT_DIR/mcp-dispatcher.sh"
ALLOWED_COMMANDS_FILE="$SCRIPT_DIR/allowed-commands.txt"
DEFAULT_ALLOWED_COMMAND="/usr/bin/xcrun mcpbridge"
# --- メイン処理 ---
log_step "SSH ディスパッチャのセットアップを開始"
# --- 0. 前提条件:macOS Remote Login (sshd) の確認 ---
log_step "前提条件の確認"
# macOS は launchd ソケットアクティベーションで sshd を管理するため、
# プロセスの有無ではなくポート 22 のリッスン状態で判定する
if ! nc -z localhost 22 2>/dev/null; then
log_error "macOS の Remote Login (SSH) が無効です"
log_error ""
log_error "以下のいずれかの方法で有効にしてください:"
log_error " 1. システム設定 > 一般 > 共有 > リモートログイン を ON"
log_error " 2. sudo systemsetup -setremotelogin on"
log_error ""
log_error "有効にした後、このスクリプトを再実行してください"
exit 1
fi
log_info "Remote Login (sshd) は有効です"
# ~/.ssh ディレクトリの確認・作成
if [ ! -d "$SSH_DIR" ]; then
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
log_info "$SSH_DIR を作成しました"
fi
# --- 1. リポジトリ内ファイルの確認 ---
log_step "ディスパッチャファイルの確認"
if [ ! -f "$DISPATCHER_SCRIPT" ]; then
log_error "ディスパッチャスクリプトが見つかりません: $DISPATCHER_SCRIPT"
exit 1
fi
chmod 755 "$DISPATCHER_SCRIPT"
if [ ! -f "$ALLOWED_COMMANDS_FILE" ]; then
log_error "ホワイトリストファイルが見つかりません: $ALLOWED_COMMANDS_FILE"
exit 1
fi
log_info "ディスパッチャ: $DISPATCHER_SCRIPT"
log_info "ホワイトリスト: $ALLOWED_COMMANDS_FILE"
# 登録済みコマンドの表示
log_info "現在の許可コマンド:"
grep -v '^#' "$ALLOWED_COMMANDS_FILE" | grep -v '^$' | while read -r cmd; do
log_info " - $cmd"
done
# --- 2. authorized_keys への登録 ---
log_step "authorized_keys の設定"
# コンテナ用の SSH 鍵ペアを確認・生成
CONTAINER_KEY="$SSH_DIR/container_ed25519"
if [ ! -f "$CONTAINER_KEY" ]; then
ssh-keygen -t ed25519 -f "$CONTAINER_KEY" -N "" -C "apple-container-mcp"
log_info "コンテナ用 SSH 鍵ペアを生成しました: $CONTAINER_KEY"
else
log_info "コンテナ用 SSH 鍵ペアは既に存在します(スキップ)"
fi
PUBLIC_KEY=$(cat "$CONTAINER_KEY.pub")
DISPATCHER_ENTRY="command=\"$DISPATCHER_SCRIPT\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding $PUBLIC_KEY"
# authorized_keys ファイルの作成(なければ)
if [ ! -f "$AUTHORIZED_KEYS" ]; then
touch "$AUTHORIZED_KEYS"
chmod 600 "$AUTHORIZED_KEYS"
log_info "$AUTHORIZED_KEYS を作成しました"
fi
# 既に登録済みか確認(公開鍵の本体部分で照合)
KEY_BODY=$(echo "$PUBLIC_KEY" | awk '{print $2}')
if grep -qF "$KEY_BODY" "$AUTHORIZED_KEYS" 2>/dev/null; then
# パスが変わっている場合は既存エントリを更新する
EXISTING_LINE=$(grep -F "$KEY_BODY" "$AUTHORIZED_KEYS")
if [ "$EXISTING_LINE" = "$DISPATCHER_ENTRY" ]; then
log_info "公開鍵は既に authorized_keys に登録済みです(スキップ)"
else
grep -vF "$KEY_BODY" "$AUTHORIZED_KEYS" > "$AUTHORIZED_KEYS.tmp"
echo "$DISPATCHER_ENTRY" >> "$AUTHORIZED_KEYS.tmp"
mv "$AUTHORIZED_KEYS.tmp" "$AUTHORIZED_KEYS"
chmod 600 "$AUTHORIZED_KEYS"
log_info "authorized_keys のディスパッチャパスを更新しました"
fi
else
echo "$DISPATCHER_ENTRY" >> "$AUTHORIZED_KEYS"
log_info "ディスパッチャ経由の公開鍵を authorized_keys に追加しました"
fi
# --- 完了 ---
log_step "セットアップ完了"
log_info "ディスパッチャ: $DISPATCHER_SCRIPT"
log_info "ホワイトリスト: $ALLOWED_COMMANDS_FILE"
log_info "SSH 鍵: $CONTAINER_KEY"
log_info ""
log_info "動作確認:"
log_info " 許可: ssh -i $CONTAINER_KEY localhost $DEFAULT_ALLOWED_COMMAND"
log_info " 拒否: ssh -i $CONTAINER_KEY localhost echo hello"
container/Dockerfile(イメージ定義)
コンテナのイメージを定義する Dockerfile です。パッケージのインストール、ユーザー作成、Claude Code のインストールまでをビルド時に行います。イメージのビルドだけで環境が整うので、コンテナ作成後の追加セットアップは不要です。
FROM ubuntu:24.04
# --- apt パッケージ ---
COPY packages.txt /tmp/packages.txt
RUN apt-get update -qq \
&& xargs -a /tmp/packages.txt apt-get install -y -qq \
&& rm -rf /var/lib/apt/lists/* /tmp/packages.txt
# --- GitHub CLI ---
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update -qq \
&& apt-get install -y -qq gh \
&& rm -rf /var/lib/apt/lists/*
# --- Node.js 22.x ---
RUN curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh \
&& bash /tmp/nodesource_setup.sh \
&& apt-get install -y -qq nodejs \
&& rm -f /tmp/nodesource_setup.sh \
&& rm -rf /var/lib/apt/lists/*
# --- claude ユーザー作成(sudo 付き) ---
RUN useradd -m -s /bin/bash claude \
&& echo "claude ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/claude \
&& chmod 440 /etc/sudoers.d/claude
# --- Claude Code インストール(非 root) ---
USER claude
RUN mkdir -p ~/bin \
&& npm install --prefix ~/claude-code @anthropic-ai/claude-code \
&& ln -sf ~/claude-code/node_modules/.bin/claude ~/bin/claude
USER root
ENV PATH="/home/claude/bin:${PATH}"
# --- エントリーポイント ---
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
COPY packages.txt でパッケージ一覧を取り込んで xargs でまとめてインストールしています。パッケージの追加・削除が packages.txt の編集だけで済むので管理が楽です。Claude Code は非 root ユーザー claude のホームディレクトリにローカルインストールし、~/bin/claude にシンボリックリンクを作成しています。
container/packages.txt(パッケージ宣言)
apt でインストールするパッケージを 1 行 1 パッケージで宣言するファイルです。
openssh-client
curl
git
sudo
Dockerfile 内にパッケージ名をハードコードせず、外部ファイルに分離しています。パッケージを追加したい場合はこのファイルに 1 行追加してイメージを再ビルドするだけです。
container/entrypoint.sh(ランタイム初期化)
コンテナ起動時に実行される ENTRYPOINT スクリプトです。ビルド時には行えないランタイム固有の処理を担当します。
#!/bin/bash
# container/entrypoint.sh
# コンテナ起動時のランタイム初期化
# /etc/hosts 設定、~/.claude/projects シンボリックリンクなど実行時に必要な処理
set -e
# --- /etc/hosts にホスト名を追加 ---
# Apple Container のデフォルトサブネット 192.168.64.0/24 のゲートウェイ IP
# container system dns が正しく動作すれば不要になる可能性あり
HOST_ENTRY="192.168.64.1 host.container.internal"
if ! getent hosts host.container.internal &>/dev/null; then
echo "$HOST_ENTRY" >> /etc/hosts
fi
# --- ~/.claude/projects をシンボリックリンク ---
# auto-memory 書き込みのため read-write マウントから symlink
CONTAINER_USER="${CONTAINER_USER:-claude}"
CONTAINER_HOME="/home/$CONTAINER_USER"
HOST_CLAUDE_PROJECTS="/opt/host-claude-projects"
CLAUDE_DIR="$CONTAINER_HOME/.claude"
if [ -d "$HOST_CLAUDE_PROJECTS" ]; then
su - "$CONTAINER_USER" -c "mkdir -p '$CLAUDE_DIR'" 2>/dev/null || true
rm -rf "$CLAUDE_DIR/projects"
ln -sf "$HOST_CLAUDE_PROJECTS" "$CLAUDE_DIR/projects"
fi
exec "$@"
ビルド時と実行時の責務を分離しているのがポイントです。パッケージインストールやユーザー作成のように変わらない処理は Dockerfile で行い、/etc/hosts の編集やホスト設定のシンボリックリンクのように実行環境に依存する処理は entrypoint.sh で行います。最後の exec "$@" によって、container create で渡したコマンド(sleep infinity)が PID 1 として実行されます。
apple-container-start.sh(ホスト側エントリーポイント)
ホスト macOS で実行するメインスクリプトです。イメージがなければビルドし、コンテナが存在しなければ新規作成、既にあれば再開してコンテナ内のシェルに接続します。
#!/bin/bash
# scripts/apple-container-start.sh
# ホスト macOS で実行するエントリーポイント
# イメージビルド → コンテナの新規作成 or 再開 → シェル接続
#
# プロジェクトディレクトリを /workspace にマウントするため、
# プロジェクト内の .mcp.json / .claude/ / CLAUDE.md がそのまま使える
set -euo pipefail
# --- ヘルパー関数 ---
log_info() { echo "[INFO] $*"; }
log_error() { echo "[ERROR] $*" >&2; }
log_step() { echo ""; echo "==> $*"; }
# コンテナが exec 可能になるまで待機する
wait_for_ready() {
local name="$1"
local i
for i in $(seq 1 30); do
if container exec "$name" true 2>/dev/null; then
return 0
fi
sleep 1
done
log_error "コンテナ '$name' が起動しませんでした"
exit 1
}
# --- 定数 ---
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CONTAINER_NAME="${CONTAINER_NAME:-github-app-container}"
IMAGE_NAME="${IMAGE_NAME:-github-app}"
CONTAINER_USER="${CONTAINER_USER:-claude}"
HOST_USER="$(whoami)"
WORKSPACE="/workspace"
# --- 前提条件チェック ---
log_step "前提条件の確認"
if ! command -v container &>/dev/null; then
log_error "container コマンドが見つかりません"
log_error "Apple Container をインストールしてください: https://github.com/apple/container"
exit 1
fi
# --- SSH ディスパッチャのセットアップ ---
log_step "SSH ディスパッチャのセットアップ"
"$SCRIPT_DIR/setup-ssh-dispatcher.sh"
# --- イメージのビルド ---
log_step "コンテナイメージの確認: $IMAGE_NAME"
if container image list 2>/dev/null | awk '{print $1}' | grep -qx "$IMAGE_NAME"; then
log_info "イメージ '$IMAGE_NAME' は存在します(ビルドスキップ)"
else
log_step "イメージをビルド: $IMAGE_NAME"
container build \
--tag "$IMAGE_NAME" \
--file "$PROJECT_DIR/container/Dockerfile" \
"$PROJECT_DIR/container/"
log_info "イメージのビルドが完了しました"
fi
# --- コンテナの作成 or 再開 ---
log_step "コンテナの状態確認: $CONTAINER_NAME"
if container list 2>/dev/null | grep -q "$CONTAINER_NAME"; then
log_info "コンテナ '$CONTAINER_NAME' は既に実行中です"
elif container list --all 2>/dev/null | grep -q "$CONTAINER_NAME"; then
log_info "停止中のコンテナを再開: $CONTAINER_NAME"
container start "$CONTAINER_NAME"
wait_for_ready "$CONTAINER_NAME"
log_info "コンテナを再開しました"
else
log_step "新規コンテナを作成: $CONTAINER_NAME"
# --ssh: SSH agent forwarding(コンテナから GitHub 等への SSH 接続に使用)
# --volume: プロジェクトをマウント(.mcp.json, .claude/, CLAUDE.md を共有)
# --mount: ~/.claude/projects を read-write でマウント(auto-memory 書き込み用)
# sleep infinity: コンテナを running 状態で維持する(ENTRYPOINT 経由で実行)
container create --name "$CONTAINER_NAME" \
--cpus 4 --memory 4g \
--ssh \
--volume "$PROJECT_DIR:$WORKSPACE" \
--mount "type=bind,source=$HOME/.claude/projects,target=/opt/host-claude-projects" \
"$IMAGE_NAME" \
sleep infinity
container start "$CONTAINER_NAME"
wait_for_ready "$CONTAINER_NAME"
log_info "コンテナを作成・起動しました"
fi
# --- gh 認証トークンの取得 ---
GH_TOKEN=""
if command -v gh &>/dev/null && gh auth token &>/dev/null; then
GH_TOKEN="$(gh auth token)"
log_info "gh 認証トークンを取得しました"
fi
# --- コンテナシェルに接続 ---
log_step "コンテナシェルに接続"
log_info "Claude Code を起動するには: claude --dangerously-skip-permissions"
exec container exec -it \
-e "HOST_USER=$HOST_USER" \
-e "GH_TOKEN=$GH_TOKEN" \
--user "$CONTAINER_USER" \
--workdir "$WORKSPACE" \
"$CONTAINER_NAME" \
bash -lc "ulimit -n 64000 && exec bash"
いくつかのポイントを補足します。
container createでは--ssh、--volume、--mountを指定しています。--sshはホストの SSH エージェントをコンテナに転送するオプションで、コンテナからホストへの SSH 接続に使います。--volumeはプロジェクトディレクトリをコンテナの/workspaceにマウントし、.mcp.json、.claude/、CLAUDE.mdをコンテナ内でもそのまま使えるようにします。--mountは~/.claude/projectsを read-write でマウントし、Claude Code の auto-memory 機能による書き込みを可能にしています。sleep infinityは ENTRYPOINT 経由で実行されます。container createのコマンド引数として渡したsleep infinityが、entrypoint.sh のexec "$@"によって PID 1 として起動し、コンテナを running 状態に維持します。container exec -eで環境変数HOST_USERとGH_TOKENをコンテナに渡しています。container execの-eオプションを使うことで、コンテナを再作成せずにトークンの更新が反映されます。--workdir "$WORKSPACE"でシェルの初期ディレクトリを/workspaceに設定しています。コンテナに入った瞬間からプロジェクトディレクトリにいるため、すぐに作業を始められます。- コンテナシェルへの接続時に
--user claudeを指定しているのもポイントです。su -でユーザーを切り替える方法だとSSH_AUTH_SOCKがリセットされてしまい、SSH エージェントへの接続が途切れます。container exec --userなら環境変数を引き継いだままユーザーを切り替えられます。 - カスタムビルドイメージ(
IMAGE_NAME)を使用するため、ubuntu:latestを毎回セットアップする必要がなくなりました。初回のビルド時間はかかりますが、2 回目以降はビルド済みイメージからすぐにコンテナを作成できます。
使い方はとてもシンプルです。
./scripts/apple-container-start.sh
これだけで、初回はイメージのビルドからコンテナ作成まで自動で行われ、コンテナ内のシェルに接続されます。イメージのビルドには数分かかりますが、2 回目以降はビルド済みイメージを使うのですぐにコンテナが起動します。シェル内で claude --dangerously-skip-permissions を実行して Claude Code を起動してください。exit でシェルを抜けてもコンテナは起動したまま残るため、再接続はスクリプトを再実行するだけです。
volume mount で共有されるもの
プロジェクトディレクトリを /workspace にマウントしているため、以下のファイルがコンテナ内でもそのまま利用できます。
.claude/-- settings.json, settings.local.json などのプロジェクトレベル設定.mcp.json-- MCP サーバー定義(コンテナ内でHOST_USER環境変数が展開される)CLAUDE.md-- プロジェクト固有の指示.git/config-- リポジトリレベルの git user 設定
加えて、--mount オプションで ~/.claude/projects をコンテナにマウントしています。Claude Code の auto-memory 機能がプロジェクトごとの学習内容を ~/.claude/projects/ 配下に保存するため、この領域だけは read-write でマウントする必要があります。設定ファイル(settings.json 等)はプロジェクトレベル(/workspace/.claude/)を参照するため、グローバルの ~/.claude をマウントする必要はありません。
.mcp.json を volume mount で共有しているため、claude mcp add のような手動登録は不要です。コンテナを再作成しても MCP 設定はそのまま引き継がれます。
ハマりどころ
root で --dangerously-skip-permissions が拒否される
DNS の問題を解決して Claude Code をインストールした後、意気揚々と claude --dangerously-skip-permissions を実行したところ、「--dangerously-skip-permissions cannot be used with root/sudo」というエラーメッセージが表示されました。
Apple Container はデフォルトで root ユーザーとしてシェルを起動するのですが、--dangerously-skip-permissions はセキュリティ上の理由で root 権限での実行を明示的に拒否する設計になっています。考えてみれば当然で、root 権限 + 確認プロンプトなしというのはリスクが大きすぎます。
解決策は非 root ユーザーを作成して切り替えることです。起動スクリプトでは container exec --user claude を使ってユーザーを切り替えています。
Apple Container 固有の制約
Apple Container はまだ pre-1.0 ということもあり、Docker とは異なる制約がいくつかあります。開発中に遭遇したものをまとめておきます。
container cp に相当するコマンドが存在しません。Docker の docker cp のようにホストとコンテナ間でファイルをコピーする手段がないため、Dockerfile の COPY 命令か volume mount に頼ることになります。
container create に長期実行プロセスを指定しないとコンテナがすぐに終了します。sleep infinity のようなプロセスを指定して running 状態を維持してください。
動作確認
セットアップが完了したら動作確認をしてみましょう。
ホスト上での直接テスト
まずホスト macOS 上で mcpbridge が正常に動作するか確認します。Xcode を起動してプロジェクトを開いた状態で以下を実行してください。
(printf '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}\n'; sleep 3) | xcrun mcpbridge
xcode-tools を含む JSON レスポンスが返ってくれば正常です。mcpbridge は stdin が閉じると自動で終了しますが、止まらない場合は Ctrl+C で終了してください。
コンテナ内からのテスト
コンテナ内のシェルで以下を実行します。
(printf '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}\n'; sleep 3) | timeout 15 ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes "${HOST_USER}@host.container.internal" /usr/bin/xcrun mcpbridge
ホスト上での直接テストと同じ JSON レスポンスが返れば SSH ブリッジは正常に動作しています。
Claude Code からの接続確認
最後に Claude Code を起動して Xcode MCP が使えることを確認します。
claude --dangerously-skip-permissions
Claude Code 内で「現在のプロジェクトのビルドターゲット一覧を教えてください」のように依頼してみてください。Claude Code が xcode-tools MCP サーバーを呼び出し、Xcode から情報を取得できれば接続は成功です。

既知の問題
fd リーク問題
Claude Code のようにファイル操作が多いツールを Apple Container 内で実行すると、Virtualization.framework の virtiofs デバイスがファイルディスクリプタを大量消費し、ホスト macOS のアプリがクラッシュする可能性があります。
この問題は Apple Container Issue #1097 で追跡されており、v0.9.0 時点で未解決です。ulimit -n 64000 を設定しておくことで回避できます。起動スクリプトではこの設定を自動的に適用しています。
DevContainer + Docker との比較
バックエンド開発の視点で Docker と比較してみます。
Apple Container も --publish (-p) によるポートフォワーディングに対応しており、コンテナ内の dev サーバーにホストのブラウザからアクセスできます。この点は Docker と同等です。
一方、マルチサービスのオーケストレーションには差があります。Docker Compose ならデータベースやキャッシュを含む複数コンテナを docker-compose.yml 1 ファイルで定義でき、コンテナ名がそのまま DNS 名として解決されます。Apple Container でも Compose サポートが開発中でコンテナ間通信も可能になっていますが、Docker Compose ほどの成熟度にはまだ達していません。
またローカルの Dockerfile を GitHub Actions 等の CI でそのまま流用できるため、環境差異が起きにくいです。Apple Container は macOS 専用なので Linux ベースの CI では使えません。
ただし、今回の目的は Claude Code を隔離環境で安全に動かすことです。ビルドやテストは全てホストの Xcode で実行されるため、コンテナ内で dev サーバーを立てたり複数サービスを連携させたりする必要がありません。上記の制約はいずれも問題にならず、個人的には Apple Container の方がセットアップが手軽で楽に感じました。
まとめ
Apple Container を使うことで、Docker Desktop なしで VM レベルの分離環境を構築し、Claude Code を --dangerously-skip-permissions 付きで実行できました。SSH ディスパッチャとホワイトリストの組み合わせで Xcode MCP への接続を安全に制限しつつ、volume mount によってプロジェクト設定(.mcp.json、.claude/、CLAUDE.md)をコンテナ内でも共有できる構成になっています。
ただし Apple Container はまだ 0.9.0 で Docker とは異なる制約が残っています。Dockerfile ベースの構成で対応できる範囲ではありますが、業務での利用を考えるなら同様の構成を DevContainer + Docker Desktop で構築するほうが安定していて安心かもしれません。
今後 AI を活用した開発環境を整備していくなかで、Apple Container でも無理なく採用できる構成かどうかという視点は常に持っておきたいと思っています。成熟が進めば、macOS ネイティブならではの軽量さとセキュリティを兼ね備えた有力な選択肢になるはずです。
セキュリティ上の制限について
ここまで「隔離環境で安全に動かす」と述べてきましたが、この構成でもセキュリティ上の懸念があります。
Apple Container(Linux VM)内で Claude Code 自体は隔離されています。しかし Xcode MCP 経由でホスト Mac の Xcode を呼び出すため、Simulator はホスト Mac 上で動作します。つまり Claude Code が生成したコードはホスト Mac 上の Simulator で実行されることになり、VM による境界が実質的に意味をなさない状況になります。
具体的には、Simulator からホスト Mac の SSH 鍵・AWS 認証情報・環境変数・ファイルシステムへのアクセスが可能です。Apple Container 内で DNS 制限をかけたとしても Simulator のトラフィックには効きません。コンテナ内の Claude Code を隔離しても、その出力物が隔離されていない環境で動く以上、完全なサンドボックスとは言えないわけです。
この構成は「ファイルシステムを直接操作するツールの暴走を防ぐ」用途には有効ですが、生成コードの実行まで含めた強固な分離が必要な場合は macOS を利用できる Tart VM などの別のアプローチを選ぶことになるかと思います。