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.prisma と src/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 で name と description を書いた 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.local の PORT= で各 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 が両方とも生きるのが気持ちいい。