新しいworktreeを切って、さあ作業しようとした瞬間にCannot find module。ああ、またnode_modulesが無いやつだ、とnpm installを回してその間にコーヒーを一杯淹れる。worktreeを3つ、5つと並べて複数ブランチを同時に触るようになると、この「淹れる時間」が1日に何度も発生するようになる(コーヒー消費量とworktreeの本数が比例していく様子、誰かグラフにしてくれないだろうか)。
複数のIssueやブランチを並行して進めたいときにgit worktreeは便利だが、Node.jsプロジェクトで使うと必ずこの壁にぶつかる。ブランチは軽量にコピーできるのに、依存関係だけは毎回フルインストールが要る。そしてインストールしたnode_modulesは、worktreeを消さない限りディスクに居座り続ける。
この記事で学べること
- git worktreeで
node_modulesが共有されず、毎回複製される仕組み上の理由 - 「ディスクを食う問題」と「セットアップに時間がかかる問題」が別物である理由
- symlink共有・パッケージマネージャのstore/cache活用・自動インストールの3つの対処法とその向き不向き
前提条件
- git worktreeで複数ブランチを並行して作業している(npm / pnpm / yarnのいずれか)
package-lock.json/pnpm-lock.yaml/yarn.lockのいずれかでロックファイルを管理している
実際、このブログを書いているリポジトリのコミット履歴を見ても、同じ1日のうちに昼と夜でそれぞれ別のブランチがdevへマージされている記録が残っている。マージのたびに新しいworktreeで次のブランチの作業を始めるなら、そのたびにどこかでnpm installかnpm ciを踏むことになる。1日1回で済む作業ではない。
なぜnode_modulesは複製されるのか
git worktreeは、1つの.gitオブジェクトデータベースを複数の作業ディレクトリで共有する仕組みだ。だから同じコミット履歴・同じブランチ構成を、ディスク容量をほぼ増やさずに何本も並行して持てる。ここまでは軽い。
身軽になれるはずなのに、node_modulesだけはこの恩恵を受けない。ほとんどのNode.jsプロジェクトの.gitignoreには、次のような記述が入っている。
# dependencies
/node_modules
つまりnode_modulesはそもそもgitの管理対象外で、worktree間で共有されるオブジェクトDBにも入っていない。git worktree addは新しい作業ディレクトリにブランチの中身をチェックアウトするだけで、依存関係のインストールまでは面倒を見てくれない。実際、AIエージェントに開発を任せる環境の失敗パターンを分析した記録にも、この現象がそのまま挙がっている。
新しいworktreeを作成した直後は
node_modulesが存在しない。SKILL.mdに「worktree作成後はnpm ciを実行する」という手順が明記されていなければ、この失敗はセッションをまたいで繰り返される。
人間が手を動かす場合は「あ、また忘れてた」で済むが、無人実行のタスクやAIエージェント経由の並行開発だと、この抜け漏れがそのまま失敗として積み上がる。実際、この抜け漏れを機械的に検出するために書かれたチェックはこういう形をしている。
if [[ -f package-lock.json ]] && [[ ! -d node_modules ]]; then
npm ci
fi
worktreeを1つ登録するたびに、こういうチェックを踏むかどうかで「気づいたら詰んでいた」と「静かに解消していた」が分かれる。そして、たとえこのチェックを毎回通したとしても——node_modulesは確実にworktreeの数だけ物理的に複製される。3本のworktreeなら3倍、10本なら10倍。掛け算だけは律儀に守られる、というのがディスク消費の根本原因だ。
対処1: symlinkで一本化する
一番手軽なのは、メインのworktreeにnode_modulesを1つだけ持たせて、他のworktreeからsymlinkを張る方法だ。
# 追加したworktree側で実行
rm -rf node_modules
ln -s ../main-worktree/node_modules node_modules
ディスク消費はほぼゼロになるが、これが安全に機能するのはブランチ間でpackage.jsonの依存関係が完全に一致している場合だけだ。片方のブランチでだけ新しいパッケージを追加していたら、symlink先のインストール漏れがそのまま「動くはずのコードが動かない」原因になる(原因調査の第一容疑者がsymlinkだと気づくまで、意外と時間がかかる)。「とりあえずsymlinkしておけば安心」ではなく、依存が分岐した瞬間にsymlinkを解除して個別インストールに戻す運用とセットで使う必要がある。
対処2: パッケージマネージャのstore/cacheを共有する
symlinkの脆さが気になるなら、パッケージマネージャ側の仕組みで解決する手もある。npm/yarn/pnpmのどれを使うかを自動判定するスクリプトのロジックを見ると、判定の起点はだいたいロックファイルの有無だ。
pm="npm"
[[ -f "yarn.lock" ]] && pm="yarn"
[[ -f "pnpm-lock.yaml" ]] && pm="pnpm"
この3つのうちpnpmは、依存パッケージの実体をグローバルなcontent-addressableのstoreに1回だけ保存し、各プロジェクトのnode_modulesはそこへのハードリンクで構成する設計になっている。同じバージョンの同じパッケージなら、worktreeが何本に増えても実体のディスク消費は増えない。パッケージマネージャの選定でpnpmがデフォルトに据えられているツールも少なくなく、あるSkillsエコシステムの設定ファイルでも「パッケージマネージャーを指定できます(デフォルト: pnpm)」という形でpnpmが既定値になっている例がある。
npm/yarnのままpnpmに乗り換えたくない場合でも、キャッシュの置き場所を明示的に確認しておく価値はある。ダウンロード・ビルド済みのキャッシュが共有されていれば、worktreeごとのnode_modules自体は複製されても、インストールにかかる時間は短縮できる。
ちなみに、ロックファイルのコンフリクト解消は--theirsで機械的に受け入れて再生成するのが定石だ。
pnpm-lock.yaml|--theirsで受け入れ |pnpm installで再生成
これは「ロックファイルは正とせず、再インストールすれば復元できるもの」という前提があるからこそ成立する運用で、node_modules自体も同じ発想——ディスク上の実体は使い捨てで良く、正はロックファイルとキャッシュ/storeの側にある——で扱うと、対処の優先順位がすっきりする。
対処3: worktree作成時にインストールを自動化する
ディスクは減らせなくても、体感の待ち時間だけならgit worktree addをラップする関数で減らせる。ロックファイルの種類を見て使うパッケージマネージャを自動判定し、そのままインストールまで済ませてしまう。
wtadd() {
git worktree add "$@" || return 1
local dir
dir=$(git worktree list --porcelain | grep -A0 "^worktree" | tail -1 | cut -d' ' -f2)
(
cd "$dir" || exit 1
if [[ -f pnpm-lock.yaml ]]; then pnpm install
elif [[ -f yarn.lock ]]; then yarn install
elif [[ -f package-lock.json ]]; then npm ci
fi
)
}
これはディスク圧迫そのものの解決にはならない点に注意してほしい。あくまで「cdした瞬間にCannot find moduleを見て、そこから手動でインストールを打つ」という一手間を先回りして消すだけだ(正直、人間はnpm installを打ち忘れる生き物なので、忘れないための仕組みに変えておくほうが確実)。ディスクを減らしたいなら対処1・2と、待ち時間を減らしたいなら対処3と、狙う効果が違う。両方欲しければ組み合わせる。
| 手法 | 何が効くか | 注意点 |
|---|---|---|
| symlinkで共有 | ディスク消費をほぼゼロに | package.jsonが分岐間で完全一致する場合のみ安全 |
| パッケージマネージャのstore/cache共有(pnpm等) | ディスク消費を大幅削減、installも速い | 既存プロジェクトからの移行コストがかかる |
| worktree作成時の自動install | セットアップ時間を削る | ディスク消費は減らない。あくまで体感の"待ち"対策 |
動作確認
読者の手元でも、次のコマンドで自分の環境の実態を確認できる。
# 登録されているworktree一覧とパスを確認する
git worktree list
# 気になるworktreeに移動し、node_modules配下のファイル数を確認する
# (ディスクサイズそのものを見たい場合はduコマンドが使えるOSならdu -shでも同じ傾向が見える)
cd <worktree-path>
find node_modules -type f | wc -l
# pnpmを使っている場合、実体を持つstoreの場所を確認する
pnpm store path
# npmを使っている場合、キャッシュの場所を確認する
npm config get cache
node_modules配下のファイル数はworktreeの数だけ同じような値が積み上がるはずだが、pnpmのstoreやnpmのキャッシュは1箇所にまとまっているはずだ。この差分が、対処2で説明した「実体は1つ、参照だけ増える」設計の効果そのものになる(ちなみに、この数字を一度見てしまうとdu -shを見なかったことにはできなくなる)。
注意点・Tips
- symlink共有は依存が一致している間だけの応急処置: ブランチ間でpackage.jsonが分岐したら即座に解除する運用とセットにする
- pnpmへの切り替えはlockfile形式が変わる: 既存npm/yarnプロジェクトでは移行コストと天秤にかけて判断する
- 自動インストールのフックは無人実行のタスクほど効く: 手が空いたときに自分で打つコマンドを減らすより、worktree作成の導線に組み込んでおくほうが、AIエージェント経由の並行開発では効果が大きい
まとめ
git worktreeは.gitのオブジェクトDBを共有できても、.gitignoreされているnode_modulesまでは共有してくれない。worktreeを並べれば並べるほど、ディスク消費とセットアップの待ち時間は静かに積み上がっていく。ディスクを減らしたいならsymlinkかパッケージマネージャのstore/cache共有、待ち時間を減らしたいなら自動インストールのフック——狙う効果によって手を使い分けるのがポイントだ。以前fzfでworktreeとブランチを一括整理するエイリアスを書いたときも、片付け忘れたworktreeがディスクを侵食する話に軽く触れたが、今回はそのnode_modules自体を減らす側にフォーカスした。似たテーマで、worktreeを増やしすぎてサンドボックスのコマンドライン長上限に達しBashごと止まる話も書いたが、あれはOSのexec呼び出しが壊れる別種の障害で、今回のディスクとセットアップ時間の話とは原因も対処もまったく別物だ。あなたのプロジェクトでも、複数のworktreeを並行運用しているなら、一度find . -name node_modules -type d -exec du -sh {} +を叩いて実態を確認しておく価値はある(*/node_modulesのようなワイルドカードは、今いる場所に1階層下の候補が無ければマッチ数ゼロになり、fishやzshではそれだけでno matchesエラーとなりコマンドごと止まる。運良くマッチしても拾えるのは1階層下までで、worktreeを.claude/worktrees/<name>/のようにネストして配置している場合は取りこぼす。findならどこにいてもディレクトリ構成に関係なく拾える)。AIエージェントに複数タスクを同時に振って開発を進める比重が増えている場合、こうした環境整備も含めて型を作るところから相談したいならAI実装支援も選択肢の一つになる。



