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の配置単位は一致する

Turborepoの公式ドキュメントは、ワークスペースをapps/(アプリケーションとサービス)とpackages/(それ以外のライブラリやツール)に分ける構成から始めることを勧めています。create-turboで作るリポジトリもこの形で、ルートのturbo.jsonがタスクの定義を持ち、実行されるbuildやlintのスクリプトは各パッケージのpackage.json側にあります。

Claude Codeの公式ドキュメントが示すモノレポの例も同じ形です。ルート直下に全体向けのCLAUDE.mdを置き、packages/api・packages/web・packages/sharedにはそれぞれ専用のCLAUDE.mdを置きます。apps/*・packages/*の切り方が、そのままCLAUDE.mdを置く単位になります。

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.yamlのpackages欄、npmやyarnならpackage.jsonのworkspaces欄)で宣言します。

Turborepoはapps/aとapps/a/bのようなネストしたパッケージ構成を、パッケージマネージャごとの挙動が曖昧になるためサポートせず、この構造はエラーになります。CLAUDE.mdをどこに置くか迷ったときは、package.jsonの位置を境界の目安にできます。

Task Graphが決めるのは実行順で、CLAUDE.mdの読み込みではない

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

くらべる

同じ`apps/web`と`packages/ui`でも、動く軸が違う

実行順

Task Graph(Turborepo)

webのbuildがuiのbuildに依存していれば、uiのビルドが終わるまでwebのビルドは待たされます。決まるのは、どのタスクをどの順で走らせるかとキャッシュの扱いです。

読み込み

CLAUDE.md階層(Claude Code)

起動ディレクトリの祖先にあるCLAUDE.mdは起動時に読み込まれます。サブディレクトリのCLAUDE.mdは、Claudeがそのディレクトリのファイルを読んだ時点で追加されます。uiのファイルを一度も開かなければ、ui/CLAUDE.mdは載らないままです。

dependsOnの書き方ごとに、Claudeが別パッケージに触れる可能性は変わります。

dependsOnの記法Turborepoでの意味CLAUDE.mdへの影響
^buildTurborepoでの意味直接の依存パッケージのbuildを先に実行CLAUDE.mdへの影響依存先のCLAUDE.mdは、Claudeがそのパッケージのファイルを読んだ時点で読み込まれる
build(^なし)Turborepoでの意味同一パッケージ内の別タスクへの依存CLAUDE.mdへの影響追加で連結されるCLAUDE.mdはない
utils#buildTurborepoでの意味特定パッケージの特定タスクへの依存CLAUDE.mdへの影響Claudeがutils配下を開かない限り、utils/CLAUDE.mdは連結されない

Task Graphを目で見たいときはturbo run build --graphです。ファイル名を付けなければdot形式のグラフが標準出力に出ます。--graph=my-graph.svgのように拡張子を付ければ、svg・html・mermaidで書き出せます。パッケージ数の多いモノレポでCLAUDE.mdの配置漏れを疑うときは、Claude Code側の/contextの「Memory files」欄に、実際に読み込まれたCLAUDE.mdが一覧で出ます。

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

packages/uiがbuildタスクを持ち、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=...uiが使えます。uiとそれに依存するパッケージが対象になります。反対に--filter=web...と後ろに付けると、webとその依存先が対象です。

--affectedを付けると、既定では--filter=...[main...HEAD]と同じ、mainとHEADの差分を基準にした絞り込みになります。特定パッケージだけなら--filter=@repo/uiのように名前で指定します。複数の--filterは和集合として扱われ、--filter=...ui --filter=[HEAD^1]なら、どちらかに当たるパッケージが対象です。

パッケージのディレクトリにcdしてturbo buildを実行すると、そのパッケージを起点にした自動スコープが働きます。--filterを書かずに範囲が絞られ、--filterを明示した場合はそちらが優先されます。packages/apiで作業しているときの素のturbo buildは、そのパッケージのbuildタスクを走らせます。dependsOnで必要な依存先のタスクも実行されます。

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

TurborepoのcacheDirの既定値は.turbo/cacheです。ビルド成果物のアーカイブが溜まる場所ですが、Claudeが読む必要はありません。Claude Codeのコンテンツ検索は.gitignoreに従うため、.turboやdistが.gitignoreに載っていれば、追加設定なしで検索結果から外れます。チェックイン済みの生成物やベンダーSDKのように.gitignoreの外にあるパスだけ、permissions.denyで明示的に止めます。

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

パターンの末尾を/**/*にすると、ディレクトリ自体はlsやcdの対象のまま、中のファイルだけが読めなくなります。範囲は設定ファイルの置き場所で変わります。リポジトリ全員に効かせるなら.claude/settings.json、自分だけなら.claude/settings.local.jsonです。

注意点が2つあります。1つ目はdenyの効く範囲です。Readのdenyは組み込みのファイルツールのほか、cat・head・tail・sedのようなファイルを名指しするコマンドにも効きます。一方、grep -r pattern .のようにファイル名を指定しない読み方や、Pythonスクリプトが自分でファイルを開く読み方は止められません。キャッシュの中身を絶対に読ませたくないなら、OSレベルで止めるサンドボックスが必要です。

2つ目は相対パターンの基準です。./から始めるパターンは、ルートではなくセッションを起動したディレクトリを基準にします。プロジェクトの.claude/settings.jsonは親ディレクトリから継承されません。packages/webから起動する運用なら、パッケージごとの.claude/に同じ設定を置く方法があります。もう1つは、リポジトリルートの.claude/settings.local.jsonに//始まりの絶対パスで書く方法で、こちらは起動ディレクトリに関係なく読まれます。

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

別パッケージを編集するとき、CLAUDE.mdは一緒に読まれるか

apps/webから起動して、依存先のpackages/uiも直させたい場面は、Package Graphの辺をまたぐ作業です。起動ディレクトリの外にあるため、Claudeに別のディレクトリへのアクセスを与える必要があります。そのとき、与え方によってCLAUDE.mdが読まれるかどうかが変わります。

与え方CLAUDE.mdとrulesの読み込みSkillsの読み込み
additionalDirectories設定CLAUDE.mdとrulesの読み込み読まれないSkillsの読み込み読まれない
--add-dirフラグ・/add-dirCLAUDE.mdとrulesの読み込み環境変数CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1を付けたときだけSkillsの読み込み読まれる

--add-dirのヘルプはv2.1.289で次のように出ます。

claude --help
  --add-dir <directories...>            Additional directories to allow tool
                                        access to

--add-dirの項目には、CLAUDE.mdを読むかどうかの記述はありません。読ませたいときは次のように環境変数を付けます。

CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 claude --add-dir ../../packages/ui

additionalDirectoriesは常にCLAUDE.mdを読み込みません。そのため.claude/settings.jsonに書いて全員に配る方式は、packages/ui/CLAUDE.mdの指示がほしい用途には向きません。アクセスだけ共有できればよいならadditionalDirectories、指示も読ませるなら起動時に--add-dirと環境変数を組み合わせます。

どのCLAUDE.mdがいつ読まれたかをhookで記録する

ここまで書いた「祖先は起動時、サブディレクトリはファイルを読んだとき」は、InstructionsLoadedイベントを使うと、自分のリポジトリで目に見える形にできます。読み込まれた指示ファイルごとに発火し、file_pathとload_reason、遅延読み込みならtrigger_file_pathが渡されます。load_reasonはsession_start・nested_traversal・path_glob_match・include・compactのいずれかです。

手順

読み込みの順序を記録する

  1. 1

    ログ出力のhookを足す

    .claude/settings.local.jsonにInstructionsLoadedのhookを書きます。matcherにはload_reasonが使えるので、遅延読み込みだけに絞れます。

  2. 2

    パッケージから起動してファイルを読ませる

    packages/apiからclaudeを起動し、packages/uiのファイルを読むよう頼みます。

  3. 3

    ログを突き合わせる

    ルートとpackages/apiのCLAUDE.mdはsession_startで、packages/ui/CLAUDE.mdは遅延読み込みとして記録され、trigger_file_pathに読んだファイルが入ります。dependsOnでつながっていてもuiのファイルを読んでいなければ、ログに出ません。

{
  "hooks": {
    "InstructionsLoaded": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "jq -c '{load_reason, file_path, trigger_file_path}' >> /tmp/claude-instructions.log"
          }
        ]
      }
    ]
  }
}

このイベントに決定権はなく、読み込みを止めたり内容を変えたりはできません。監査ログや観測のための用途です。/compactのあとにも、再読み込みされたファイルがcompactとして記録されます。

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

Turborepoのtestタスクは、turbo.jsonでdependsOn: ["build"]のように、同じパッケージのbuildに依存させる書き方があります。Claude Code側も、パッケージごとにテストの流儀が違うなら、packages/api/.claude/skills/のようにパッケージ配下にSkillを置けます。そのパッケージの作業に関係すると判断されたときに読み込まれるので、他パッケージの作業中はコンテキストを消費しません。具体的な書き方はClaude Codeモノレポのテスト戦略をSKILL.mdで教える手順で扱っています。

よくあるつまずき

つまずき

設定したのに期待どおりにならないとき

  • キャッシュヒットなのに成果物のディレクトリが空

    タスクの順序が正しくても、outputsが空だとファイルはキャッシュされず、ヒットしても出力が復元されません。ビルドタスクを足したらoutputsも書きます。

  • ビルドが意図しない順序で走る

    dependsOnの^buildとbuildを取り違えていないか、表の記法と見比べます。取り違えると、依存が抜けたビルドがキャッシュされることもあります。

  • filterの角括弧を忘れた

    Gitを基準にするフィルタは角括弧が必須です。--filter=HEAD^1では、Gitの範囲指定として認識されません。

  • 依存パッケージの指示がClaudeに効いていない

    依存先のファイルをまだ開いていないのが原因として考えられます。そのパッケージのファイルを開かせるか、必要な指示をルートのCLAUDE.mdに書きます。

まとめ

配置に迷ったら、InstructionsLoadedのログで実際の読み込みを見てから、ルートに書く指示とパッケージに書く指示を分けるという進め方があります。

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