1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code × git worktree で「複数 VS Code を並列に動かす」開発環境を作った話

1
Posted at

1 つのモノリシック Next.js リポジトリの中に 4 つのドメインが同居している個人プロジェクトで、ドメインごとに VS Code ウィンドウと Claude Code セッションを分けて並列開発する仕組みを作った。git worktree + Claude Code の skill / hook / slash command を組み合わせて、コンテキスト分離・共有層保護・出荷ゲートの 3 つを自動化した記録。

TL;DR

  • 1 リポジトリ × N ドメインのモノリスで並列開発するには git worktree + Claude Code の skill が効く
  • ドメイン別に skill を切り、各 worktree で Claude が「自分の担当範囲」を宣言してから動くようにする
  • 共有層(Prisma スキーマ、共通 lib)への編集はガード skill で止める
  • /ship という自作スラッシュコマンドで typecheck → lint → build → push を 1 発で回す
  • MCP サーバーは 前回記事の alwaysLoad で頻用のものだけ常時ロード、それ以外は遅延ロードに

背景

私が個人で開発している Next.js アプリは、機能スコープの異なる 4 つのドメインが 1 つのモノリスに同居している。仮にこの 4 つを A / B / C / D と呼ぶ。

src/
├── app/
│   ├── members/
│   │   ├── A/   ← ドメイン A の画面
│   │   ├── B/   ← ドメイン B の画面
│   │   ├── C/   ← ドメイン C の画面
│   │   └── ...
│   └── api/
│       ├── A/   ← ドメイン A の API
│       ├── B/
│       └── ...
├── lib/         ← 全ドメイン共通(prisma / auth / 認可ユーティリティ)
└── ...
prisma/schema.prisma   ← 9 つのドメインモデルが同居(763 行)

ドメインごとのフォルダ分離は綺麗にできているが、共通基盤(prisma/schema.prismasrc/lib/*)が 全ドメイン共依存。これに気付かず複数 VS Code を開いて同時編集すると、Prisma の migration 衝突や型崩壊で泣く。

加えて、Claude Code のコンテキストも問題になる。1 つのセッションが 4 ドメイン全部のファイルを読み込むと、すぐ context 窓が埋まる。長くなれば /compact で済むが、それより そもそも担当ドメインだけ読ませた方が速くて正確

やったこと

レイヤー設計

5 つのレイヤーで切った:

レイヤー 担当 具体物
L1 ファイル分離 git worktree ドメインごとに作業ディレクトリ
L2 環境分離 .env.local / PORT dev server を独立に起動
L3 コンテキスト分離 Claude skill domain-a / domain-b ... をスコープ宣言
L4 共有層保護 Claude skill shared-layer-warden が prisma/lib への編集を止める
L5 出荷ゲート Claude slash command /ship = typecheck → lint → build → push
┌────────── 1 Mac / 1 Git Repo ──────────┐
│                                        │
│  ~/projects/myapp           ← main     │
│  ~/projects/myapp.a         ← worktree │
│  ~/projects/myapp.b         ← worktree │
│  ~/projects/myapp.c         ← worktree │
│  ~/projects/myapp.d         ← worktree │
│                                        │
│  どれも同じ .git を共有                  │
│  作業ファイルだけ独立                     │
└────────────────────────────────────────┘

┌─ VS Code #1 (a) ─ Claude session A ─ port 3001 ─┐
┌─ VS Code #2 (b) ─ Claude session B ─ port 3002 ─┐
┌─ VS Code #3 (c) ─ Claude session C ─ port 3003 ─┐
┌─ VS Code #4 (d) ─ Claude session D ─ port 3004 ─┐
┌─ VS Code #0 (main) ─ Claude session E ─ 3000 ───┐

各 Claude は自分の skill だけを autoload
共有 MCP は alwaysLoad で全 session 共通

Step 1: git worktree でドメイン別ディレクトリ

cd ~/projects/myapp
git fetch origin
for d in a b c d; do
  git worktree add -b feature/$d-bootstrap "../myapp.$d" origin/main
done
git worktree list

.git は本体だけ。他 4 つは gitdir pointer なのでディスクは作業コピー分しか食わない。

Step 2: ブートストラップスクリプト

各 worktree に .env.local を配置し、npm install + prisma generate + ポート上書きまで一発で済ませる。

#!/usr/bin/env bash
# scripts/setup-worktree.sh
set -euo pipefail
domain="${1:?domain required}"
port="${2:-3000}"
src="$HOME/projects/myapp"
wt="$HOME/projects/myapp.$domain"

cp "$src/.env.local" "$wt/.env.local"
if grep -q '^PORT=' "$wt/.env.local"; then
  sed -i.bak "s/^PORT=.*/PORT=$port/" "$wt/.env.local" && rm -f "$wt/.env.local.bak"
else
  printf '\nPORT=%s\n' "$port" >> "$wt/.env.local"
fi

( cd "$wt" && npm install && npx prisma generate )
echo "✅ $wt ready on port $port"
./scripts/setup-worktree.sh a 3001
./scripts/setup-worktree.sh b 3002
./scripts/setup-worktree.sh c 3003
./scripts/setup-worktree.sh d 3004

Step 3: ドメイン別 skill

ここがポイント。Claude Code の skill は、frontmatter で namedescription を書いた SKILL.md を .claude/skills/<skill-name>/ に置くと自動で読み込み候補になる。

ドメインごとに「触ってよいパス」「触ってよい Prisma モデル」「触ってはいけないパス」を 明文化 する:

---
name: domain-a
description: ドメイン A 開発時に使う。スコープ・モデル・API ルート・触ってよいパスと禁止パスを宣言する
---

# domain-a

## スコープ(編集 OK)
- src/app/members/a/**
- src/app/api/a/**
- src/components/a/**

## 触ってよい Prisma モデル
- ModelA1 / ModelA2 / ModelA3

## 触ってはいけないもの(共有層)
- prisma/schema.prisma
- src/lib/auth-util.ts / src/lib/project-access.ts / src/lib/prisma.ts
- 他ドメインのフォルダ(b / c / d)

## 作業を始めるときの宣言
最初に「現在 a worktree で作業中。スコープは members/a と api/a のみ」と
宣言してから作業に入る。スコープ外を触る必要が出たら別タスク化する。

各 worktree (myapp.a) で起動した Claude は、その worktree 配下の .claude/skills/domain-a/ を見つけて読み込む。Claude が 自分自身に「私はドメイン A 担当です」と言わせることで、スコープから外れる依頼が来たときに Claude 自身が立ち止まれる。

Step 4: 共有層 Warden

ドメイン skill だけだと、Claude が善意で「あ、prisma/schema.prisma も直しますね」と動いてしまう余地が残る。これを止めるのが shared-layer-warden skill:

---
name: shared-layer-warden
description: prisma/schema.prisma と src/lib/* (auth/project-access/prisma/roles) のいずれかを編集しようとしたら、ドメイン worktree なら停止して main worktree への移動を促す
---

# shared-layer-warden

ドメイン worktree (myapp.a, myapp.b, ...) にいて、以下のいずれかへの
Edit / Write が発生する直前に手を止め、ユーザーに確認する:

- prisma/schema.prisma
- src/lib/prisma.ts
- src/lib/auth-util.ts
- src/lib/project-access.ts
- src/lib/roles.ts

確認テンプレ:
> ⚠ 共有層 (X) の編集が必要そうです。
> ドメイン worktree で共有層を直接編集すると、他 worktree との
> rebase で migration や型が衝突します。
> 安全な手順は:
>   1. main worktree に移動
>   2. そこで共有層変更の PR を出す
>   3. merge 後、各ドメイン worktree で sync する
> このまま続行しますか?

main worktree (~/projects/myapp) では発火しない。共有層変更の正規ホーム。

description が一般的すぎると効かない(autoload のヒット率が下がる)ので、具体的なファイルパスを description に書くのが効果的だった。

Step 5: /ship で出荷ゲート

.claude/commands/ship.md を置くと、Claude セッションで /ship を打てるスラッシュコマンドになる。

---
description: 現在の worktree で typecheck + lint + build を走らせ、緑なら git push
---

# /ship

以下を順に実行し、どこかで失敗したら即停止して原因を報告:

1. `git status` — 未コミット差分があれば commit を促す
2. `git rev-parse --abbrev-ref HEAD` — main なら停止
3. `npx tsc --noEmit` — 型エラー 0
4. `npm run lint` — エラー 0(warning は許容)
5. `npm run build` — ビルド成功
6. `git diff origin/main...HEAD --stat` を提示
7. ユーザー承認後 `git push -u origin <branch>`

注意: --no-verify 禁止。途中飛ばし禁止。

VS Code の Claude チャットで /ship と打つだけで、Claude が順番に実行・失敗時は原因を即報告してくれる。人間が git push を直叩きする運用と違って、失敗の原因を Claude が読み解いて報告までやってくれるのが大きい。

Step 6: PostToolUse フックで即時型チェック

.ts / .tsx を編集した直後に対象ファイルだけ tsc --noEmit を走らせる。

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/typecheck-on-edit.sh" }
        ]
      }
    ]
  },
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "alwaysLoad": true
    }
  }
}

.claude/hooks/typecheck-on-edit.sh:

#!/usr/bin/env bash
set -u
payload="$(cat || true)"
if command -v jq >/dev/null 2>&1; then
  file="$(printf '%s' "$payload" | jq -r '.tool_input.file_path // empty')"
else
  file="$(printf '%s' "$payload" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
fi
case "$file" in
  *.ts|*.tsx)
    [ -f tsconfig.json ] && npx tsc --noEmit --pretty false 2>&1 | grep -F "$file" | head -20
    ;;
esac
exit 0

これで Claude が Edit / Write した直後に そのファイルの型エラーだけ が Claude に返る。/ship を待たずに小さなループで気付ける。

Step 7: 朝の sync スクリプト

#!/usr/bin/env bash
# scripts/sync-worktrees.sh
set -e
cd "$HOME/projects/myapp"
git fetch origin
for d in a b c d; do
  wt="$HOME/projects/myapp.$d"
  [ -d "$wt" ] || continue
  ( cd "$wt" && git rebase origin/main ) || echo "⚠ rebase conflict in $d"
done

朝にこれを 1 回叩くだけで全 worktree が origin/main に追従する。共有層が main 経由で更新されたあとも、各ドメイン worktree が自動で取り込めるようになる。

1 日の運用イメージ

朝   ./scripts/sync-worktrees.sh        # 全 worktree を origin/main に追従
     code ~/projects/myapp.{a,b,c,d}    # 4+1 ウィンドウ起動

日中 各 VS Code で Claude セッション → domain-* スキルの宣言下で作業
     npm run dev で各 worktree が独自ポートで動く → ブラウザで動作確認

夕方 各 worktree で /ship → 緑なら push
     main で gh pr create → review → merge
     他 worktree で sync で取り込み

ハマったポイント・気付き

① skill の description は「具体的に」書く

description: 共有層を保護する だと autoload のヒット率が低い。description: prisma/schema.prisma, src/lib/auth-util.ts ... のいずれかを編集しようとしたら停止する のように 発火条件を文字列レベルで具体化 すると、Claude が「いま自分のやろうとしている操作が該当しそうだ」と判断しやすくなった。

/ship は Husky pre-push より良い

Husky で pre-push に同じチェックを入れる手もあるが、自作スラッシュコマンドの方が:

  • 失敗時の出力を Claude が読んで原因要約してくれる
  • Claude セッション内で完結(ターミナル切替不要)
  • スキップしたい例外時に対話で交渉できる

人間が直叩きするときのフェイルセーフとしては Husky を併用する余地あり。

③ alwaysLoad は本当に頻用のものだけ

前記事で書いた通り、alwaysLoad: true は context 常時消費とのトレードオフ。私の場合 docs/ナレッジ取得の Notion MCP だけ常時。GitHub MCP / Drive MCP は遅延ロードのままで困っていない。

④ worktree の罠: Prisma の生成物

node_modules/.prisma は worktree ごとに違うので、npm install & npx prisma generate必ず各 worktree でやる必要がある。scripts/setup-worktree.sh で自動化したけど忘れがち。

⑤ ポート衝突は早めに固定

.env.localPORT= で各 worktree のポートを 固定しておくと、npm run dev 同時起動時の衝突がなくなる。動的に空きポートを使う方式にすると、ブラウザの localStorage / OAuth callback がずれて事故る。

まとめ

「1 リポジトリ × N ドメイン」を git worktree でファイル分離 + Claude skill でコンテキスト分離 + slash command で出荷ゲート という構造に整理することで、

  • 4 つの VS Code ウィンドウで Claude を並列に走らせても context が衝突しない
  • 共有層への不用意な変更が止まる
  • push 前のチェックが Claude セッション内で完結する

という体験になった。モノレポ化(pnpm workspaces / turborepo)まで踏み込まずとも、worktree と skill だけでかなり戦える。

skill / hook / slash command の組み合わせは「Claude 自身に 自分の振る舞いを宣言 させる」設計なので、人間用のドキュメントを書きながら Claude も従う、という二重利用ができる。CLAUDE.md と skill が両方とも生きるのが気持ちいい。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?