Claude Media
Claude CodeでTurborepoのモノレポを設定する — turbo.jsonとCLAUDE.mdの対応

Claude CodeでTurborepoのモノレポを設定する — turbo.jsonとCLAUDE.mdの対応

Turborepoのturbo.jsonが作るタスクグラフと、Claude CodeがCLAUDE.mdを読み込む階層は別の仕組みです。両者の対応を設定例とコマンドで示します。

Turborepoはturbo.jsonに書いたタスクの依存関係でモノレポのビルド順を決めるツールです。一方Claude Codeは、起動したディレクトリから祖先をたどってCLAUDE.mdを連結します。この2つの階層は別の仕組みで動いているため、どちらか一方だけを見て設定すると、パッケージ間で指示が抜け落ちたりキャッシュが効かなかったりします。この記事では、turbo.jsonのタスクグラフとCLAUDE.mdの配置をどう対応させるかを、設定例とコマンドで扱います。Claude Codeのモノレポ運用全般(権限のパッケージスコープやclaudeMdExcludesを含む設計)はClaude Codeモノレポ設計 — CLAUDE.md階層とパッケージ単位の権限スコープで扱っており、本稿はTurborepo固有のturbo.jsonとの対応に絞ります。

Turborepoの構成とCLAUDE.mdの配置単位は一致する

create-turboで新規に作るTurborepoリポジトリは、ルートにturbo.jsonpackage.jsonを置き、apps/配下にデプロイ可能なアプリケーション、packages/配下に共有ライブラリを配置する構成が既定です。ルートのturbo.jsonはタスクの定義だけを持ち、実際に実行されるbuildlintのスクリプトは各パッケージのpackage.json側にあります。

Claude Codeの公式ドキュメントが示すモノレポの例も、同じ形をしています。ルート直下に全体向けのCLAUDE.mdを置き、packages/apipackages/webpackages/sharedのようなパッケージごとに、そのパッケージ専用のCLAUDE.mdを配置する構成です。Turborepoのapps/packages/という単位と、CLAUDE.mdを配置する単位は同じディレクトリ境界を指しています。つまり、turbo.jsonでタスクを定義する前に、どのパッケージにCLAUDE.mdを置くかはapps/*packages/*の切り方でほぼ決まります。

monorepo/
  turbo.json
  package.json
  CLAUDE.md                 # ルート全体の指示
  apps/
    web/
      CLAUDE.md              # webアプリ固有の指示
      package.json
  packages/
    ui/
      CLAUDE.md               # 共有UIパッケージ固有の指示
      package.json

Turborepoがパッケージとして認識するのは、apps/*packages/*の直下にpackage.jsonがあるディレクトリだけです。この境界はワークスペース側の設定(pnpm-workspace.yamlpackages欄やnpm/yarnのpackage.jsonworkspaces欄)で宣言します。さらにTurborepoはapps/aapps/a/bのようなネストしたパッケージ構成をサポートしません。両方にpackage.jsonを置くとエラーになります。CLAUDE.mdをどこに置くか迷ったときは、このpackage.jsonの位置を境界の基準にします。ネストが許されない以上、CLAUDE.mdの階層もパッケージごとにフラットな1段構成で揃えられます。

Package GraphとTask Graphは、CLAUDE.mdの階層とは別の軸

Turborepoは2種類のグラフを内部に持ちます。Package Graphはパッケージマネージャがpackage.jsonの依存関係から作る構造で、あるパッケージが別のパッケージをインストールしていれば、その依存関係がそのままグラフの辺になります。Task Graphはturbo.jsondependsOnで表現するタスク同士の依存関係で、buildtestといったタスクをノード、依存をエッジとした有向非巡回グラフ(DAG)です。

ここが誤解しやすい点です。Task GraphのdependsOnは、あるパッケージのタスクが完了するまで別のタスクを待たせるという実行順の制御にすぎません。CLAUDE.mdがどのタイミングでコンテキストに連結されるかは、Claudeが実際にそのディレクトリのファイルを読んだかどうかで決まり、Task Graphの依存順とは連動しません。webパッケージのbuilduiパッケージのbuildに依存していても、Claudeがui配下のファイルを一度も開かなければ、ui/CLAUDE.mdはセッションに読み込まれないままです。

dependsOnの書き方によって、Claudeがそのパッケージへ実際に触れる可能性も変わります。

dependsOnの記法意味CLAUDE.mdへの影響
^build意味依存パッケージのbuildを先に実行CLAUDE.mdへの影響依存先パッケージのCLAUDE.mdは、Claudeがそのパッケージのファイルを読んだ時点でオンデマンド読み込みされる
build(^なし)意味同一パッケージ内の別タスクへの依存CLAUDE.mdへの影響追加で連結されるCLAUDE.mdはない(すでに起動時か直前の読み込みで載っている範囲)
utils#build意味特定パッケージの特定タスクだけへの依存CLAUDE.mdへの影響Claudeがutils配下を実際に開かない限り、utils/CLAUDE.mdは連結されない

タスクの依存構造を目で確認したい場合は、turbo run build --graphでグラフを可視化できます。パッケージ数が多いモノレポでCLAUDE.mdの配置漏れを疑ったときは、このグラフと/contextコマンドの「Memory files」欄を突き合わせると、どのパッケージのCLAUDE.mdが実際に読み込まれているかを確認できます。

turbo.jsonとCLAUDE.mdを揃えて書く

packages/uibuildタスクを持ち、apps/webがそれに依存する構成を例にします。turbo.json側では次のように依存関係とキャッシュ対象を定義します。

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": []
    }
  }
}

outputsを設定しないと、Turborepoはそのタスクの成果物を一切キャッシュしません。次に実行したときにキャッシュヒットしても、ファイル自体は復元されないため、ビルドは通ったのにファイルが無いという状態になります。

CLAUDE.md側は、このタスク構造をなぞる必要はありません。apps/web/CLAUDE.mdには「このアプリはNext.jsで書かれている」「packages/uiのコンポーネントを優先して使う」のようなパッケージ固有の指示を書き、packages/ui/CLAUDE.mdにはコンポーネントの命名規約やエクスポートのルールを書きます。ルートのCLAUDE.mdには、パッケージ横断で守ってほしい方針だけを残します。パッケージ固有の指示をルートに書き足していくと、Claude Codeが起動時に読み込む量が増えるだけでなく、他チームが担当するパッケージの指示まで拾ってしまいます。この混入を止めるclaudeMdExcludesの書き方はclaudeMdExcludesの設定手順 — モノレポで他チームのCLAUDE.mdを除外するで扱っています。

turbo run --filterでClaudeの変更範囲を検証する

Claude Codeにパッケージ横断のリファクタリングをさせたあと、影響範囲を絞って検証したいことがあります。Turborepoの--filterフラグは、この検証に直接使えます。

turbo build --filter=[HEAD^1]

これは直前のコミットからの差分を基準に、変更のあったパッケージとその依存先だけをビルドします。ブランチ間の差分で絞りたい場合は--filter=[main...my-feature]のように書き換えます。特定パッケージだけを対象にしたいときは--filter=@repo/uiのようにパッケージ名を指定し、そのパッケージに依存している側まで含めたい場合は--filter=...uiと先頭にドットを3つ付けます。

パッケージのディレクトリにcdしてからturbo buildを実行すると、そのパッケージを起点にした自動スコープが働き、--filterを書かずに同じ範囲へ絞り込めます。ただし--filterを明示した場合は、この自動スコープより--filter側が優先されます。Claude Codeがpackages/apiから起動しているセッションでは、素のturbo buildを叩くだけでそのパッケージのTask Graphだけが実行される、という理解でよい場面が多くなります。

.turboキャッシュと成果物をClaudeに読ませない

Turborepoの既定のキャッシュ保存先は.turbo/cacheです。ここにはビルド成果物のアーカイブが大量に溜まりますが、Claudeが読む必要はありません。Claude Codeのコンテンツ検索は.gitignoreに従うため、.turbodistがすでに.gitignoreに載っていれば追加設定なしで検索対象から外れます。チェックインされている生成物やベンダーSDKのように.gitignoreの外にあるパスだけ、permissions.denyで明示的に止めます。

{
  "permissions": {
    "deny": [
      "Read(./**/.turbo/**/*)",
      "Read(./**/dist/**/*)",
      "Read(./**/.next/**/*)"
    ]
  }
}

パターンの末尾を/**/*にしているのは、ディレクトリ自体はlscdできる状態を残しつつ、中のファイルだけを読ませないためです。この設定をリポジトリ全員に効かせたい場合は.claude/settings.jsonに、自分だけの設定なら.claude/settings.local.jsonに書きます。denyルールはClaude組み込みのファイルツールと、Claude Codeが認識するcatgrepのようなBashコマンドには効きますが、ファイルを自前で開くサブプロセスまでは止められません。

パッケージ数が数百規模のTurborepoでは、Globツールによるファイル探索自体が既定のタイムアウトに達することがあります。探索範囲を絞りきれない場合はCLAUDE_CODE_GLOB_TIMEOUT_SECONDSで上限を引き上げる選択肢があります。

パッケージ単位のテストタスクとSkillsを揃える

Turborepoのtestタスクはturbo.jsondependsOn: ["build"]のように書くのが一般的です。ビルド成果物に依存するテストが多いためです。Claude Code側でも、パッケージごとにテストの書き方が異なるなら、packages/api/.claude/skills/のようにパッケージ配下にSkillを置き、そのパッケージで作業するときだけ読み込まれるようにできます。具体的な書き方はClaude Codeモノレポのテスト戦略をSKILL.mdで教える手順で扱っています。

よくあるつまずき

  • outputsを書き忘れてキャッシュが効かない: dependsOnだけ設定してタスクの実行順は正しくなっても、outputsが空だとファイルはキャッシュされません。ビルドタスクを追加したらoutputsも必ずセットで書きます。
  • ^buildbuildの違いを取り違える: ^buildは依存パッケージのタスク、先頭の^がないbuildは同一パッケージ内のタスクを指します。書き間違えると、意図しないパッケージが先に実行されたり、依存が抜けて壊れたビルドがキャッシュされたりします。
  • --filter[]を書き忘れる: ソースコントロールを基準にしたフィルタ([HEAD^1][main...my-feature])は、角括弧で囲まないと認識されません。角括弧なしで--filter=HEAD^1と書くとパッケージ名の指定として扱われ、エラーになります。
  • CLAUDE.mdをタスクの依存順で読み込まれると思い込む: dependsOnはビルドの実行順であって、CLAUDE.mdの連結順ではありません。依存パッケージのCLAUDE.mdを確実に読ませたいなら、Claudeにそのパッケージのファイルを明示的に開かせるか、必要な指示をルートのCLAUDE.mdに書きます。

まとめ

Turborepoのturbo.jsonが作るTask Graphは、タスクの実行順とキャッシュ対象を決めるための仕組みです。Claude CodeのCLAUDE.md階層は、Claudeが起動したディレクトリからの祖先関係とファイルアクセスの実績で決まります。両者は同じapps/*packages/*という境界を共有しますが、連結される順序やタイミングは独立しています。turbo run --graph/contextのMemory files欄を突き合わせながら、パッケージごとにCLAUDE.mdと.turboキャッシュの扱いを揃えていくのが、Turborepoのモノレポ運用の起点になります。

この記事を共有:XはてブLinkedIn