モノレポでClaude Codeが暴走しない設計:pnpm×Turborepoで影響範囲を固定する
モノレポでClaude Codeに「いい感じに直して」は事故のもと。pnpm workspaceとTurborepo affectedで依存と影響範囲を固定し、安全にレビューできる差分にする方法を実例で。
「このモノレポ、依存まわりをいい感じに整理しといて」
そう頼んだ僕に、Claude Codeは期待どおりの仕事をしてくれました。重複していたユーティリティを packages/shared に集約し、importも書き換え、ローカルのビルドも通った。完璧に見えたんです。
CIが真っ赤になるまでは。apps/web は動くのに apps/api が起動しない。原因は、共通化したつもりの User 型が、実はWeb側とAPI側で別物だったこと。1つに無理やりまとめたせいで、両方が静かに壊れていました。
このとき痛感したのは、モノレポでAIが事故るのは賢さの問題じゃないということです。境界が曖昧なリポジトリは、AIにとって「どこまで触っていい場所か分からない巨大なフォルダ」でしかありません。賢いほど広く読んで、広く書き換えて、広く壊す。だから僕は方針を変えました。Claude Codeに賢く推測させるのをやめて、推測しなくていい状態を先に作ることにしたんです。
この記事は、記事サイト・教材ページ・管理画面・検索まわりを1つのリポジトリで回している僕が、Claude Codeを「作業者」ではなく「構造を守る相棒」に変えるためにやったことの記録です。pnpm workspace、Turborepo/Nxのaffected、CODEOWNERS、依存検査スクリプトまで、コピペで動く形で置いていきます。
この記事の要点
- モノレポでClaude Codeが暴走するのは、能力ではなく境界(パッケージの依存ルール)が未定義だから。先に地図とルールを作れば事故は激減する。
- 初回プロンプトでは「修正して」ではなく「まだ編集しないで、依存と循環と共通化しすぎを一覧化して」と頼む。提案フェーズを分けるとGit差分が汚れない。
- 内部依存は
workspace:*で固定。apps/*からapps/*、packages/*からapps/*への依存は禁止し、スクリプトで機械的に検査する。 - CIは全件ビルドではなく Turborepo/Nx の affected(変更影響範囲だけ実行) に絞る。
fetch-depth: 0を忘れると差分が取れず全件扱いになる。 - 一番効いたのは技術選定ではなく、
packages/sharedを触るとき影響アプリをClaude Codeに列挙させるという運用ルールだった。
そもそもモノレポにする価値があるのか
モノレポとは、複数のアプリやライブラリを1つのGitリポジトリで管理する構成です。apps/web、apps/api、packages/ui、packages/shared のように分け、型・UI・設定・CIを横断して再利用します。
ただ、これは万能解ではありません。小さなLPと小さなAPIを同じ人が触っているだけなら、リポジトリを分けたままでも困りません。逆に、共通UI・共通型・共通認証・共通CIを何度もコピペしているなら、モノレポにする価値が出てきます。
僕は判断に迷ったとき、次の3つに答えられるかで決めています。
| 判断軸 | YESならモノレポ向き | NOなら分割のまま |
|---|---|---|
| 同じ変更を複数リポジトリに反映しているか | コピペ運用が頻発 | たまにしか起きない |
| パッケージ間の互換性を同じPRで確認したいか | 型やAPIが密結合 | 各々が独立 |
| レビュー責任をパス単位で分けられるか | チームや担当が明確 | 全員が全部見る |
この3つに答えられないまま移行すると、モノレポは「便利な統合」ではなく「巨大な未整理フォルダ」になります。実際、僕が最初にやらかしたのもこれでした。深く考えずに全部1つにまとめ、半年後に「この変更どこまで影響する?」を毎回説明できなくなって苦しんだんです。
Claude Codeに相談するときも、最初の質問は「モノレポを作って」ではありません。「この構成はモノレポにする価値があるか、分けたままにすべきか」です。既存のアプリ数、共有したいコード、デプロイ単位、担当チーム、CI時間を渡すと、移行すべき範囲とまだ分けておくべき範囲を整理してくれます。
コードより先に「地図」を作る
モノレポ導入で一番失敗しやすいのは、いきなり packages/shared を作ることです。共通箱を用意すると、目についた重複を何でも放り込みたくなる。でも共通化の前に確認すべきは、その重複が本当に同じ概念かです。冒頭の事故がまさにこれで、名前が同じ User でも、管理画面のユーザー・請求先のユーザー・認証セッションのユーザーは別物でした。
だから僕がモノレポで最初に作るのは、コードではなく地図です。地図なしで共通化を始めると、packages/shared が何でも置き場になり、数か月後に削れない依存が積み上がります。
graph TD
WEB["apps/web"] --> UI["packages/ui"]
WEB --> SHARED["packages/shared"]
API["apps/api"] --> SHARED
UI --> CONFIG["packages/config"]
SHARED --> CONFIG
CI["CI affected tasks"] --> WEB
CI --> API
この図で、apps/* はユーザーに近い実行単位、packages/* は再利用する部品です。そして矢印が「パッケージ境界」です。境界とは要するに「どのパッケージがどのパッケージに依存してよいか」を明文化した線のこと。Claude Codeには、この線を越えた変更を提案させないようにします。
実装の順番も固定します。地図 → 依存ポリシー → CI → 共通化、です。先に構造を読ませ、依存してよい方向を決め、境界違反を検出できる仕組みを入れ、最後に「共通化しても壊れにくいもの」だけを移す。この順番にすると、Claude Codeの出力が変わります。「重複しているのでsharedへ移動しました」ではなく、「この関数は副作用がなく apps/api に依存していないため packages/shared へ安全に移動できます」のように、理由付きの提案になるんです。
初回プロンプトは「まだ編集しないで」から始める
ここが効きます。Claude Codeに作業を任せたくなる気持ちをぐっとこらえて、初回は「読んでほしい範囲」と「守る境界」だけを渡し、編集を禁止します。以下はそのまま貼れる依頼文です。
このリポジトリをモノレポとして把握してください。
前提:
- apps/web は Next.js の画面
- apps/api は API サーバー
- packages/ui は画面部品
- packages/shared は型、バリデーション、純粋関数
- packages/config は ESLint、TypeScript、Prettier、Vitest などの共通設定
守るルール:
- apps/* から apps/* へ直接依存しない
- packages/* から apps/* へ依存しない
- 内部パッケージの依存は workspace:* を使う
- 変更後は affected tasks で lint/test/build を確認する
最初に、依存関係、危ない循環依存、共通化しすぎているファイル、
CIで確認すべきコマンドを一覧化してください。
まだ編集はしないでください。
「まだ編集はしない」の一行が地味に重要です。Claude Codeは広い文脈を読める反面、最初から修正まで任せると、既存設計を理解する前に共通化を始めてしまう。リポジトリマップを先に出させると、レビュー対象が「コード差分」ではなく「提案」になります。提案なら、間違っていてもGit履歴は1ミリも汚れません。僕はこの「提案フェーズ」を挟むようになってから、やり直しの回数が体感で半分以下になりました。
pnpm workspaceで依存の入口を固定する
pnpm workspaceは、複数パッケージを同じリポジトリ内で扱う仕組みです。内部パッケージは workspace:* で参照します。これにより、npmレジストリ上に同名パッケージがあっても、誤って外部版を解決してしまう事故を防げます。書き方の細部はpnpm workspace公式ドキュメント、Turborepoのタスク設定はTurborepo公式ドキュメントに最新仕様があるので、迷ったら一次情報を確認してください。
ルートに pnpm-workspace.yaml を置きます。
packages:
- "apps/*"
- "packages/*"
ルートの package.json には、人間もClaude Codeも迷わないスクリプト名を置きます。名前が素直だと、AIが「どのコマンドで確認すればいいか」を自分で選べるようになります。
{
"name": "acme-monorepo",
"private": true,
"packageManager": "[email protected]",
"scripts": {
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"typecheck": "turbo run typecheck",
"ci:affected": "turbo run lint test build --affected",
"check:deps": "node scripts/check-workspace-deps.cjs"
}
}
各アプリの内部依存は、バージョン番号ではなく workspace:* で書きます。
{
"dependencies": {
"@acme/shared": "workspace:*",
"@acme/ui": "workspace:*"
}
}
Claude Codeへの依頼は「依存を追加して」だけだと弱いです。設定ファイル・import・CIまで一貫させたいので、確認コマンドまで含めて頼みます。
apps/web から @acme/ui と @acme/shared を使えるようにしてください。
package.json では workspace:* を使い、直接相対パス import は避けてください。
変更後に pnpm install --lockfile-only、pnpm check:deps、pnpm ci:affected
で確認できる形にしてください。
TurborepoとNxのaffectedを使い分ける
CIを「全件ビルド」にすると安全に見えますが、毎回30分かかるCIは誰も見なくなります。だから変更影響範囲(affected)だけ走らせる。ここでTurborepoとNxを使い分けます。
| 観点 | Turborepo | Nx |
|---|---|---|
| 得意なこと | package scriptsの高速並列実行とキャッシュ | プロジェクトグラフとタスクグラフの精密管理 |
| 向く規模 | 小〜中規模のTSモノレポ | 大規模・依存関係が複雑 |
| 導入コスト | 低い(turbo.json だけ) | やや高い(initと設定) |
| 最初に選ぶなら | まずこちらで十分 | 精度がもっと必要になったら |
Turborepoの turbo.json は現行の tasks 形式で書きます。
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Nxを既存のpnpm workspaceに後入れするなら、初期化してからaffectedを使います。
pnpm dlx nx@latest init
pnpm nx affected -t lint test build --base=origin/main --head=HEAD
どちらを選んでも、Claude Codeへの指示は「全件ビルドして」ではなく「変更影響範囲だけ確認して」に寄せます。記事生成・検索・決済・管理画面が同じリポジトリにあると、この差はそのまま公開速度に直結します。僕の環境では、affectedに切り替えただけでPRごとのCI時間が平均で3分の1くらいになりました。
境界ルールはコードで検査する(コピペで動く)
口約束の境界ルールは、忙しい日に必ず破られます。だから機械にやらせます。次のスクリプトは、内部パッケージが workspace: で参照されているか、apps/* への依存が混ざっていないかを確認します。scripts/check-workspace-deps.cjs として保存すれば、node scripts/check-workspace-deps.cjs で実行できます。
const fs = require("node:fs");
const path = require("node:path");
const ROOT = process.cwd();
const WORKSPACE_DIRS = ["apps", "packages"];
const DEP_FIELDS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
];
function readJson(file) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
// apps/ packages/ 配下から package.json を持つディレクトリを集める
function findPackageDirs(baseDir) {
const absoluteBase = path.join(ROOT, baseDir);
if (!fs.existsSync(absoluteBase)) return [];
return fs
.readdirSync(absoluteBase, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(absoluteBase, entry.name))
.filter((dir) => fs.existsSync(path.join(dir, "package.json")));
}
const packages = WORKSPACE_DIRS.flatMap(findPackageDirs).map((dir) => {
const manifest = readJson(path.join(dir, "package.json"));
return { dir, name: manifest.name, manifest };
});
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
let failed = false;
for (const pkg of packages) {
for (const field of DEP_FIELDS) {
const deps = pkg.manifest[field] || {};
for (const [name, range] of Object.entries(deps)) {
const internal = byName.get(name);
if (!internal) continue; // 外部パッケージはスキップ
const fromDir = path.relative(ROOT, pkg.dir).replace(/\\/g, "/");
const toDir = path.relative(ROOT, internal.dir).replace(/\\/g, "/");
// 内部依存は必ず workspace:* で宣言する
if (!String(range).startsWith("workspace:")) {
console.error(`${pkg.name}: ${name} must use workspace:* in ${field}`);
failed = true;
}
// packages/* や apps/* が app パッケージに依存してはいけない
if (toDir.startsWith("apps/")) {
console.error(`${pkg.name}: ${fromDir} must not depend on app package ${toDir}`);
failed = true;
}
}
}
}
if (failed) process.exit(1);
console.log(`Checked ${packages.length} workspace packages.`);
Claude Codeにこのスクリプトを追加させるときは、「検査コードもテスト対象」と明記します。たとえば、わざと "@acme/api": "1.0.0"(workspace指定ではない、しかもappへの依存)を1つ仕込んだサンプルで失敗することを確認させる。そうしないと、誰も呼ばない飾りスクリプトで終わります。僕は最初これをサボって、半年間一度も実行されていない検査スクリプトを発掘したことがあります。
CODEOWNERSでレビュー責任を固定する
CODEOWNERSは、GitHubで特定パスのレビュー担当を自動指定するファイルです。Claude Codeがどれだけ良い差分を作っても、レビュー担当が曖昧ならモノレポは崩れます。「誰の許可も要らない共通パッケージ」が一番危ない。
/apps/web/ @acme/frontend
/apps/api/ @acme/backend
/packages/ui/ @acme/design-system
/packages/shared/ @acme/platform
/packages/config/ @acme/platform
/pnpm-workspace.yaml @acme/platform
/turbo.json @acme/platform
このファイルを CODEOWNERS として置いたうえで、Claude Codeには「packages/shared を触る場合は影響する apps/* を列挙して」と頼みます。共通パッケージは便利な反面、変更範囲が広いので、レビューとテストの密度を上げるべき場所です。冒頭の事故も、もし「shared変更時は影響アプリを列挙」のルールがあれば、User 型がWebとAPIの両方に効くと気づけたはずなんです。
CIチェックリストは最小から始める
最小構成のGitHub Actionsはこれです。最初から欲張らず、check:deps と ci:affected の2本だけ通します。
name: monorepo-ci
on:
pull_request:
push:
branches: [main]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm check:deps
- run: pnpm ci:affected
fetch-depth: 0 は絶対に外さないでください。Turborepoの --affected もNx affectedも、Gitの差分を見て影響範囲を決めます。履歴が浅すぎると差分が取れず、全件扱いになったり、逆に必要なタスクが漏れたりする。僕はここを fetch-depth: 1 のままにしていて、「affectedにしたのにCIが全然速くならない」と数日悩みました。原因はこれ一行でした。
実務でよくある4ケース
1. packages/ui のButtonを変更する。 「@acme/ui の公開APIを変えずにButtonのloading状態を追加し、apps/web の利用箇所をaffected範囲で確認して」と頼みます。UIの変更は見た目だけに見えて、フォーム・管理画面・LPのCTAに波及します。PRにはスクリーンショットと pnpm ci:affected の結果を添えます。
2. 認証ユーザー型を packages/shared に移す。 APIとWebで同じ UserRole を使えて重複は減りますが、共通型を肥大化させると逆に変えにくくなる。「DBモデルではなく公開DTO(APIレスポンスや画面入力で使う安全なデータ形)だけをsharedに置く」と指定します。冒頭の事故の正しい直し方が、まさにこれでした。
3. Next.jsやTypeScriptをアップグレードする。 一部だけ上げると tsconfig・ESLint・テスト環境がずれます。「packages/config から順に更新し、apps/web と apps/api の差分を分けてPR説明を書いて」と頼む。pnpm -r update で終わらせず、lockfile・型チェック・affected buildまで確認します。
4. チーム横断の機能を追加する。 請求機能のように、UI・API・shared schema・監査ログが同時に動くもの。「作業を4つのPRに分ける案」を出させると巨大PRを避けられます。モノレポはまとめて編集できるのが利点ですが、まとめてレビューする義務はありません。
僕がやらかした失敗3つ
正直に書きます。最初のモノレポ運用は事故だらけでした。
ひとつ目は冒頭の話、packages/shared を万能箱にしたこと。formatDate や UserRole まではよかった。でもDB接続・画面固有hooks・APIクライアントまで突っ込んだ結果、全パッケージがsharedに引きずられ、共通箱を1行変えると全アプリのCIが回る地獄になりました。今はClaude Codeに「sharedに置く理由と置かない理由を説明して」と毎回確認させています。
ふたつ目は、相対パスで境界を越えるimportを放置したこと。../../packages/shared/src は短期的には動きます。でもビルド順序・型解決・公開APIのレビューを静かに壊す。内部パッケージは @acme/shared のような名前でimportし、workspace:* で依存を宣言する。これを徹底してから、原因不明のビルド順序エラーが消えました。
みっつ目は、「動いたからOK」で判断したこと。モノレポでは、動いた1アプリの裏で別アプリが壊れていることが本当にあります(冒頭がそれ)。今はPR説明に「変更したパッケージ/影響するアプリ/実行したaffected tasks/未確認の理由」を必ず書かせています。機械が確認した範囲を明文化するだけで、見落としが激減しました。
PR前に使えるレビュー依頼テンプレート
最後に、PRを出す前にClaude Codeへ投げている依頼文です。そのままコピペで使えます。
今回の差分をモノレポ観点でレビューしてください。
確認してほしいこと:
- apps/* から apps/* への直接依存がないか
- packages/* から apps/* への依存がないか
- 内部依存が workspace:* になっているか
- packages/shared に置くべきでない実装が混ざっていないか
- Turborepo または Nx affected で確認すべきタスクが足りているか
- CODEOWNERS 上のレビュー担当が明確か
出力形式:
- ブロッカー
- 修正推奨
- 確認済みコマンド
- PR本文に書くべき影響範囲
このテンプレートは、Claude CodeとNx workspace、Claude Codeとpnpm workspace、Claude CodeとTurborepo、チーム開発のClaude Code運用と合わせて読むと、実務に落とし込みやすいはずです。
よくある質問
Q. 小さなプロジェクトでもモノレポにすべき? A. いいえ。共通UI・共通型・共通CIを何度もコピペしているなら価値がありますが、独立した小さなLPとAPIなら分割のままで十分です。「同じ変更を複数リポジトリに反映しているか」を最初に自問してください。
Q. TurborepoとNx、どっちを先に入れるべき?
A. 小〜中規模ならまずTurborepoです。turbo.json だけで高速化とキャッシュが効きます。プロジェクトグラフやaffectedの精度がもっと必要になったらNxを検討する、という順番が現実的です。両方を同時に深く入れると、同じタスクを二重管理してCIが読みにくくなります。
Q. workspace:* と固定バージョン、どちらで内部依存を書く?
A. 内部パッケージは必ず workspace:* です。固定バージョンだと、npm上の同名パッケージを誤って解決する事故が起きます。この記事の check:deps スクリプトを入れておけば、固定バージョン指定をCIで弾けます。
Q. CIをaffectedにしたのに速くならない。なぜ?
A. ほぼ確実に fetch-depth: 0 の設定漏れです。affectedはGit差分から影響範囲を判定するので、履歴が浅いと差分が取れず全件扱いになります。actions/checkout に fetch-depth: 0 を必ず付けてください。
Q. Claude Codeに最初から修正まで任せてはダメ? A. ダメではありませんが、初回は「まだ編集しないで、依存と循環と共通化しすぎを一覧化して」と提案フェーズを分けるほうが安全です。提案なら間違っていてもGit差分が汚れず、レビューも軽くなります。
実際に試した結果
冒頭の「両アプリ静かに崩壊」事件以来、僕はモノレポで「Claude Codeを信じるか」で悩むのをやめました。代わりに見るのは、どの境界ルールで止まったかです。
一番効果が大きかったのは、意外にも技術選定ではありませんでした。workspace:* の強制と pnpm ci:affected の標準化、そして「packages/shared を触るとき影響アプリをClaude Codeに列挙させる」という運用ルール。この3つを入れただけで、レビュー漏れと不要な全件CIがはっきり減り、記事サイトとSaaS管理画面を同じリポジトリで扱うときの安心感が段違いになりました。
賢いAIに広く任せるより、転んでもケガしない境界を先に引く。遠回りに見えて、モノレポではこれが一番速い、というのが今の実感です。
チームでClaude Codeをモノレポに導入するなら、技術選定より先に「誰がどの境界を守るか」を決める必要があります。ClaudeCodeLabでは、CLAUDE.md・CODEOWNERS・CI・レビュー依頼テンプレートまで含めたClaude Code研修・導入相談を扱っています。既存リポジトリを前提に、最初の安全なPR単位まで一緒に分解できます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeのチーム利用でコストが読めない時に作る予算ログ
チーム導入前に、誰が何に使い、どの成果が出たかを見える化する予算ログの作り方。
コミット前の3分チェック: Claude Codeが触った範囲を確認してから確定する
Claude Codeが勝手に広げた変更を、コミット前に3分で見抜く確認手順。差分の範囲、検証ログ、ステージするファイルの絞り込みを順番に解説します。
Claude Codeをチーム導入する前に作る「リスク台帳」の中身
Claude Codeを個人実験で終わらせずチーム導入するための、権限・CI・公開の事故を防ぐリスク台帳の作り方を実例とコードで解説します。