Skip to content

Instantly share code, notes, and snippets.

@hiragram
Created February 14, 2026 06:03
Show Gist options
  • Select an option

  • Save hiragram/05c8ec0f7f5ca04e7bfecae283440ef2 to your computer and use it in GitHub Desktop.

Select an option

Save hiragram/05c8ec0f7f5ca04e7bfecae283440ef2 to your computer and use it in GitHub Desktop.

Docker + Claude Code + xcode-remote アーキテクチャ

Claude Codeを --dangerously-skip-permissions で自律的に動かしつつ、ホストのMac環境を汚さず、かつXcodeビルドなどMacでしか実行できない操作も可能にする仕組みの解説。

解決したい課題

Claude Codeの --dangerously-skip-permissions は強力だが、ホスト環境で直接実行すると以下のリスクがある:

  • ファイルシステムへの無制限なアクセス
  • 任意のシェルコマンドの実行
  • 意図しないシステム設定の変更

一方、iOSアプリ開発ではXcodeのツールチェーン(xcodebuild, xcrun simctl 等)がmacOS上でしか動作しないため、単純にDockerに閉じ込めるとビルドやテストができなくなる。

この仕組みは「Dockerでサンドボックス化しつつ、Macでしかできない操作はMCP経由でリモート実行する」というアプローチでこの問題を解決する。

全体像

┌─────────────────────────────────────────────────────────┐
│  Zellij (ターミナルマルチプレクサ)                         │
│                                                         │
│  ┌──────────────┐  ┌──────────┐  ┌───────────────────┐  │
│  │ Plans        │  │ Terminal │  │ Changed Files     │  │
│  │ (ホスト側)    │  │ (Docker) │  │ (ホスト側)         │  │
│  └──────────────┘  └──────────┘  └───────────────────┘  │
│  ┌──────────────────────────┐  ┌───────────────────┐    │
│  │ Claude Code              │  │ PR Status         │    │
│  │ (Docker内で実行)          │  │ (ホスト側)         │    │
│  │ --dangerously-skip-perms │  └───────────────────┘    │
│  └────────┬─────────────────┘                           │
└───────────┼─────────────────────────────────────────────┘
            │ MCP (HTTP)
            │ host.docker.internal:19418
            ▼
┌───────────────────────┐
│  xcode-remote         │
│  (macOSホスト上で稼働)  │
│                       │
│  xcodebuild 実行      │
│  xcodegen 実行        │
└───────────────────────┘

コンポーネント詳細

1. scripts/new-worktree — ワークツリー作成スクリプト

エントリーポイント。origin/main から新しいgit worktreeを作り、開発セッションを起動する。

./scripts/new-worktree           # Docker モード(デフォルト)
./scripts/new-worktree --no-docker  # ホスト直接モード

やっていること:

  1. /usr/share/dict/words からランダムな3単語を生成してworktree名にする(例: dungy-turpid-azyme
  2. git worktree add でworktreeを作成
  3. direnv allow で環境変数を有効化
  4. scripts/docker-claude を呼び出してDocker環境を起動

worktreeごとに独立したブランチ・Dockerコンテナ・Zellijセッションが作られるため、複数タスクの並行作業が可能。

2. scripts/docker-claude — Docker環境のオーケストレーション

このスクリプトが仕組みの中核。以下を順番に行う:

2-1. OAuthトークンの管理

.secrets (リポジトリルートに配置、.gitignore対象)
├── CLAUDE_CODE_OAUTH_TOKEN          # Claude Code認証トークン
└── CLAUDE_CODE_OAUTH_TOKEN_ISSUED   # 発行日(期限管理用)
  • トークンが未設定なら claude setup-token を対話的に実行して取得・保存
  • 発行から335日以上経過していたら更新を促す(有効期限365日)
  • .secrets ファイルはメインリポジトリ直下に置かれ、全worktreeで共有される

2-2. xcode-remoteサーバーの起動

xcode-remoteはHTTPサーバーとして動作し、MCP (Model Context Protocol) 経由で xcodebuild コマンドを実行できるようにする。

  • localhost:19418 でヘルスチェック、未起動なら自動起動
  • XCODE_REMOTE_ALLOWED_PATHS で操作可能なディレクトリを制限(worktrees配下のみ)
  • ログは /tmp/xcode-remote.log に出力

2-3. MCP設定の生成

docker/mcp.docker.json を動的に生成し、コンテナ内の .mcp.json にマウントする。

{
  "mcpServers": {
    "xcode-remote": {
      "type": "http",
      "url": "http://host.docker.internal:19418/mcp"
    }
  }
}

Docker内からホストのxcode-remoteに到達するため、Dockerの特殊DNS host.docker.internal を使用する。

2-4. Dockerコンテナの起動

docker run -d --rm \
    --name "claude-${WORKTREE_NAME}" \
    -v "$PROJECT_DIR:$PROJECT_DIR" \          # プロジェクトディレクトリ(双方向)
    -v "$MAIN_GIT_DIR:$MAIN_GIT_DIR" \        # .gitディレクトリ(worktree用)
    -v "mcp.docker.json:.mcp.json" \          # MCP設定
    -v "settings.docker.json:.claude/settings.json" \  # 権限設定
    -v "$HOME/.gitconfig:ro" \                # Git設定(読み取り専用)
    -v "$HOME/.ssh:ro" \                      # SSH鍵(読み取り専用)
    -v "$HOME/.config/gh:ro" \                # GitHub CLI設定(読み取り専用)
    -v "$HOME/.claude:ro" \                   # Claude設定(読み取り専用)
    -e "CLAUDE_CODE_OAUTH_TOKEN=..." \        # 認証トークン
    claude-ios-dev sleep infinity

ポイント:

  • プロジェクトディレクトリは同じ絶対パスでマウントする。これによりDocker内とホストでパスが一致し、xcode-remoteに workingDirectory をそのまま渡せる
  • ホストの設定ファイル(.gitconfig, .ssh, .config/gh)は読み取り専用でマウント
  • コンテナ名に worktree名を含めることで、複数コンテナの並行運用が可能

2-5. Zellijセッションの起動

テンプレート zellij/claude-dev.docker.kdl 内のプレースホルダを置換し、5ペインのレイアウトで起動する。

ペイン 実行環境 役割
Plans ホスト plans/ ディレクトリの変更をリアルタイム表示
Claude Code Docker --dangerously-skip-permissions でClaude Code実行
Changed Files ホスト git diffのリアルタイム表示
Terminal Docker デバッグ用ターミナル
PR Status ホスト PRのCI状態を表示

fswatchgh コマンドなどホスト側のツールが必要なペインはホスト上で、Claude Codeとそのデバッグ用ターミナルはDocker内で実行する。

3. docker/Dockerfile — コンテナイメージ

FROM node:22-bookworm

RUN apt-get update && apt-get install -y git curl gosu jq
# GitHub CLI
RUN curl -fsSL https://cli.github.com/packages/... && apt-get install -y gh
# Claude Code
RUN npm install -g @anthropic-ai/claude-code

COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

最小限の構成。iOS開発ツール(Xcode等)はコンテナに含まれず、すべてxcode-remote MCP経由でホスト側に委譲する。

4. docker/entrypoint.sh — コンテナ初期化

コンテナ起動時に以下を行う:

  1. UID/GIDマッピング: ホストの UID:GID に合わせたユーザーを作成。ボリュームマウントされたファイルの権限問題を回避
  2. オンボーディングスキップ: ~/.claude.jsonhasCompletedOnboarding: true を書き込み
  3. ホスト設定のコピー: .claude/ ディレクトリをコピーし、プラグインパスをコンテナ内のパスに書き換え
  4. Docker専用CLAUDE.md: ユーザーレベルの ~/.claude/CLAUDE.md を書き換え、「xcodebuildは使えない、代わりにxcode-remote MCPを使え」とClaude Codeに指示
  5. 非rootユーザーで実行: gosu でUID/GIDを切り替えてコマンドを実行

5. docker/settings.docker.json — 権限設定

--dangerously-skip-permissions を使いつつ、Claude Codeの設定レベルで操作を制限する。

許可されている操作:

  • Git操作(add, commit, push, fetch, merge, reset, rebase, restore)
  • ファイル検索・閲覧(cat, grep, find, ls, tree)
  • GitHub CLI(ghコマンド)
  • xcode-remote MCP(全操作)
  • .claude//tmp/ へのファイル書き込み
  • WebFetch, WebSearch

拒否されている操作:

  • Supabase CLIの直接実行(MCP経由のみ許可)

6. scripts/cleanup-worktree — ワークツリーの掃除

不要になったworktreeを自動削除するユーティリティ。

  • 未コミットの変更がないことを確認
  • ローカルHEADがリモートにプッシュ済み、またはmainにマージ済みであることを確認
  • 条件を満たしたworktreeとそのローカルブランチを削除

パスの一致が肝

この仕組みで最も重要な設計判断の一つが、Docker内とホストで同じ絶対パスを使うこと。

ホスト:   /Users/hiragram/Development/optical-note/worktrees/dungy-turpid-azyme/
Docker内: /Users/hiragram/Development/optical-note/worktrees/dungy-turpid-azyme/

これにより:

  • Claude Codeが pwd で取得したパスをそのままxcode-remoteの workingDirectory に渡せる
  • パス変換ロジックが不要
  • gitの操作(.git ディレクトリ参照含む)がそのまま動作

セキュリティモデル

レイヤー 制限内容
Docker ファイルシステムはマウントされたディレクトリのみアクセス可能
xcode-remote XCODE_REMOTE_ALLOWED_PATHS で操作可能ディレクトリを制限
settings.docker.json Claude Codeが使えるツール・コマンドをホワイトリストで制限
ボリュームマウント ホスト設定ファイルは読み取り専用(:ro)でマウント

--dangerously-skip-permissions はClaude Codeの対話的な権限確認をスキップするだけで、settings.jsonallow/deny リストは引き続き有効。Docker自体のサンドボックスとxcode-remoteのパス制限と合わせて、三重の防御になっている。

ワークフローの全体の流れ

1. ./scripts/new-worktree を実行
     │
2. git worktree作成 (ランダム名のブランチ)
     │
3. ./scripts/docker-claude が起動
     │
     ├─ OAuthトークンの確認・取得
     ├─ xcode-remoteの起動確認
     ├─ MCP設定ファイルの生成
     ├─ Dockerイメージのビルド
     ├─ Dockerコンテナの起動
     └─ Zellijセッションの起動
          │
          ├─ [Docker] Claude Code (--dangerously-skip-permissions)
          │     │
          │     ├─ コード編集・git操作 → Docker内で直接実行
          │     └─ ビルド・テスト → xcode-remote MCP経由でホストで実行
          │
          ├─ [Docker] デバッグ用ターミナル
          ├─ [ホスト] Plans表示
          ├─ [ホスト] Changed Files表示
          └─ [ホスト] PR Status表示

4. Zellijセッションを閉じるとDockerコンテナも自動削除

5. 作業完了後、./scripts/cleanup-worktree で不要なworktreeを掃除

必要なもの

  • macOS (Xcodeツールチェーン用)
  • Docker Desktop (host.docker.internal のDNS解決に必要)
  • Zellij (ターミナルマルチプレクサ)
  • direnv (環境変数管理)
  • xcode-remote ($HOME/Development/xcode-remote に配置)
  • Claude Code のOAuthトークン(初回起動時に自動取得)
  • 1Password CLI (op コマンド、GEMINI_API_KEYの取得に使用、オプション)

FAQ

Q: なぜDocker Desktopが必要?

host.docker.internal はDocker Desktopが提供する特殊なDNSで、コンテナからホストマシンにアクセスするために使う。OrbStackなど互換環境でも動作する可能性がある。

Q: xcode-remoteは何?

HTTPサーバーとして動作し、MCP (Model Context Protocol) のインターフェースで xcodebuildxcodegen を実行できるツール。Claude Codeが直接呼べるMCPツールとして登録される。

Q: --dangerously-skip-permissions は本当に安全?

Docker内に閉じ込めているため、仮にClaude Codeが予期しない操作を行っても影響範囲はコンテナ内のマウント済みディレクトリに限定される。加えて settings.docker.json のホワイトリストとxcode-remoteのパス制限で多層的に保護している。ただし、マウントされたプロジェクトディレクトリ内のファイルは書き換え可能であるため、gitで変更を追跡・復元できる状態にしておくことが前提。

Q: 複数のClaude Codeを同時に動かせる?

はい。worktreeごとに独立したDockerコンテナとZellijセッションが作られるため、./scripts/new-worktree を複数回実行すれば並行して作業できる。xcode-remoteは共有だが、ジョブキューで管理されるため衝突しない。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment