AIと並行開発する — Claude Code の worktree 運用

こんにちは、ヨシダです。パロスではAIを活用した開発にClaude Codeを使用する機会が多くあります。

その中で、地味に効いてくるのが「待ち時間」でした。

1機能を投げて、返ってくるのを待って、レビューして、次を投げる。完全に直列です。AI が考えている間、こちらはただ待っている。

かといって、待つのがもったいないからと同じセッションで別機能の話を始めると、今度は前の機能の文脈が残っていて、見当違いの提案が混ざってきます。「その話はもう終わったんだけどな」と思いながら読むことになる。

これをどうにかしたくて行き着いたのが git worktree でした。この記事では、その運用と、実際にプロジェクトで使っている CLAUDE.md の中身を紹介します。

先に環境だけ書いておきます。Windows + pnpm の monorepo です。6章で書くハマりどころはこの前提にかなり依存するので、そこだけ頭の片隅に置いて読んでいただけるとありがたいです。

1. worktree で Context を物理的に分ける

worktree は、同じリポジトリから複数の作業ディレクトリを切り出せる Git の標準機能です。

ブランチごとに独立したディレクトリができるので、そこで別々の Claude Code セッションを立てられます。つまり、A機能を考えさせている間に、別ディレクトリでB機能の話を始められる。

claude --worktree <ブランチ名>

# または手動
git worktree add .claude/worktrees/<名前> -b <ブランチ名>

「それ、git clone を複数回すればいいのでは?」と思われるかもしれません。

違いは、履歴が1つで済むことと、ブランチ管理が一元化されることです。clone を増やすと fetch も管理もその数だけ増えていくので、日常的に何本も走らせるなら worktree のほうが楽です。

担当しているプロジェクトでは、これを「やってもいい運用」ではなく必須ルールにしています。CLAUDE.md にはこう書いてあります。

## Worktree ルール

### 作業開始(必須)

**全てのタスク(バグ修正・機能追加・リファクタリング)は、develop ブランチから
worktree を作成してから始める。develop への直接コミットは禁止。**

### 作業中の隔離(必須)

- **worktree 作業時はメインリポジトリへの書き込み禁止**:
  環境情報で「This is a git worktree」と表示されている場合、
  Primary working directory 配下のファイルのみ編集すること
- Additional working directories に表示されるメインリポジトリのパスは参照専用
- worktree の隔離を破ると並列作業時にファイル競合が発生するため、例外なく遵守する

大事なのは、上の「作業開始」より下の「隔離」のほうです。

Claude Code は worktree の中にいても、メインリポジトリを参照できてしまうことがあります。そして放っておくと、そちらに書き込みます。

3セッション走っている状態でこれをやられると、どのセッションがどのファイルを壊したのか分からなくなります。最初は「まあ、そんなに起きないだろう」と思っていたのですが、明文化して初めて止まりました。実際にどう壊れたかは6章に書きます。

2. CLAUDE.md の中身 — 一番効いたのは「Claude にやらせないこと」

並行させるとセッション数が増えるので、毎回プロジェクト説明をするのは無駄です。CLAUDE.md に文脈を置いて、自動で読ませます。

……というのは、たぶんどこでも言われている話です。

実際に書いてみて一番効果が出たのは、構成説明ではありませんでした。役割の宣言と委譲ルールです。

このリポジトリの Claude Code は **実装者ではなくオーケストレーター** として振る舞う。
最優先は「会話品質」と「コンテキスト節約」。

## Non-Goals(Claude が直接やらないこと)

- 大規模実装(目安: 10 LOC を超える実装)
- 大規模調査(コードベース横断分析・Web 調査)
- 長大ログ/大量ファイルの逐次読解

上記は必ず委譲する。

## Delegation Trigger

次のいずれかに当てはまる場合は委譲:

1. 出力が 10 行を超えそう
2. 2 ファイル以上を編集する
3. 3 ファイル以上を読む必要がある
4. 設計判断やトレードオフ比較が必要
5. Web 情報・最新情報の確認が必要

親セッションを「考えて振る人」に固定して、重い作業は別エージェントに出す。

こうすると親のコンテキストが汚れにくくなり、長時間セッションでも会話が崩れません。並列で3本回すうえでは、これが実質的な前提条件でした。

成果物の扱いも決めています。

### Save-to-file(大容量)

20 行超の成果はリポジトリ内のドキュメントディレクトリへ保存し、
会話には要約のみ戻す。

長い出力を会話に戻さない。たったこれだけですが、コンテキストの寿命がはっきり伸びます。

3. 「やってはいけないこと」を具体で書く

これは書いてみて分かったことなのですが、抽象的な禁止事項はあまり守られません。

「セキュリティに気をつけて」では弱いんですね。パターンで書きます。

## AI使用制限領域(必読)

### 原則AI禁止

- 誤りが不可逆・全体に波及する中核ロジック(具体名は案件ごとに定義)
- パスワード・秘密鍵・認証情報の取り扱い

### AIドラフトOKだが必須レビュー

- DBマイグレーション(ALTER TABLE・インデックス変更)
- 影響範囲が読みにくい箇所(具体名は案件ごとに定義)

### 禁止パターン(絶対に生成・使用しない)

- パラメータ化されていない raw SQL 実行
- `DROP TABLE` / `TRUNCATE` / WHERE 句のない `DELETE`
- 文字列をそのまま実行する系の関数
- 環境変数以外の場所での認証情報定義
- クライアントに露出する環境変数への秘密情報の格納

入力側のガードレールも、表で持っています。

データ種別 扱い
PII・顧客情報 禁止・匿名化してから使用
本番データ 禁止・サンプルで代替
秘密鍵・APIキー 絶対禁止
設計書・ソースコード 許可

「AIに任せていい/ダメ」を先に色分けしておくと、レビューのときに何を重点的に見るかが自動的に決まります。

これは並列で回すときにかなり効きます。3本同時に上がってくるとレビューが詰まるので、見る場所が先に決まっているというだけで、だいぶ気が楽になります。

4. 出力フォーマットと品質ゲートを固定する

3セッションから返ってくるものがバラバラだと、読むだけで疲れます。

なので、返し方も決めておきます。

## Output Contract to User

- 先に結論、次に根拠、最後に次アクション
- 不確実性は明示(推測・未検証・要確認を区別)
- 実施コマンド・変更ファイル・テスト結果を必ず示す

そのうえで、PR を出す前の手順も「人間側の手順」として明記しています。

## PR前の必須チェック

1. セキュリティレビュー用のカスタムコマンドを実行し、Critical 0件を確認する
2. コード品質チェック用のカスタムコマンドを実行し、スコアを記録する
3. AI生成コードの意図を自分の言葉で説明できるか確認する
4. AI禁止領域に触れていないか確認する
5. 画面・API・データ投入に変更がある場合、対応する E2E テストを同 PR で更新する

この中では、3番が本命です。

並列で回していると、「よく分からないけど動いているPR」が生まれやすくなります。テストも通っているし、レビューでも特に引っかからない。でも、なぜそう書いたのかは説明できない。

説明できないものは出さない。ルールとしてはそれだけなのですが、これが最後の砦になっています。

5. 中断と再開 — 残すのは会話ではなく状態

並行作業の最大の弱点は、3つとも中途半端なまま日をまたぐことです。

翌朝、どのセッションが何をしていたか分からなくなる。これは何度かやりました。

やっていることは単純で、区切りで WIP コミットを打ち、あわせて「今どこまでで、次に何をするか」を文章で残します。設計判断と調査結果はリポジトリ内のドキュメントディレクトリに溜める運用にしているので、再開するときはまずそこを読ませるところから始めます。

セッションそのものは復元できません。でも、状態は復元できる。

残すのは会話ではなく状態。そう割り切ってしまったほうが、結果的に速かったです。

6. ハマったところ — worktree は「作るとき」より「消すとき」が危ない

ここからは、実際に踏んだ話です。

先に前提を書いておきます。以下は Windows + pnpm の monorepo で、かつセキュリティソフトの制約により worktree 内での依存インストールが通らない、という条件で起きたことです。

macOS / Linux で worktree ごとに素直に pnpm install できる環境なら、大半は起きません。逆に言うと、条件が揃うと普通に起きます。同じような構成で開発している方がいたら、ここだけは読んでいってください。

6-1. worktree を消したら、メインリポジトリのソースが消えた

一番高くついたのがこれです。

worktree には node_modules がありません。上記の事情で worktree 単独のインストールができなかったので、メインリポジトリの node_modules へジャンクション(Windows のディレクトリリンク)を張って凌いでいました。

これ自体はちゃんと動きます。問題は、片付けのほうでした。

git worktree remove --force .claude/worktrees/<名前>
  → worktree 配下を再帰削除する
  → <worktree>/apps/web/node_modules は「メインリポジトリへのリンク」
  → その中の @myorg/db -> ../../packages/db(pnpm workspace のリンク)を辿る
  → メインリポジトリの packages/db・packages/ui のソース本体を削除

追跡ファイルが 163 件消えました。

しかも、ここが一番怖いところなのですが、このコマンドは最終的に「Directory not empty」で失敗しています。

失敗したのだから何も起きていない、と思いますよね。起きています。エラーで止まるまでの間に、削除は途中まで進みます。

さらに厄介なのは、これが git 固有の話ではないことです。rm -rf も、PowerShell の Remove-Item(再帰・強制)も、リンクを普通のディレクトリとして辿ります。別の機会には、後者でパッケージストアの中身を壊しました。

つまり「git が危ないなら手で消せばいい」は、残念ながら解決になりません。

いまは撤去手順を固定しています。

1. ジャンクションを「リンクだけ」外す
   → cmd の rmdir "<リンク>" か、Git Bash の rm "<リンク>"(-r を付けない)
   → rm -r / Remove-Item -Recurse は使わない(辿る)
2. メインリポジトリ側で git status を見て、削除(D)が 0 件であることを確認する
3. git worktree remove を --force なしで実行する
4. それでも物理ディレクトリが残るなら、空ディレクトリを robocopy /MIR でミラーして消す

4 はかなり回りくどく見えると思います。私もそう思います。

ただ、リンクを辿らずに中身だけ消せる手段が、他に見つかりませんでした。

ついでに踏んだ小さい罠も2つ書いておきます。

  • リンクは node_modules だけに潜むわけではありません。Next.js の standalone 出力は、ビルド成果物の中に外部参照のリンクを張ります。node_modules だけ確認して安心していると、足をすくわれます。
  • 「リンクがもう無いこと」の確認に fs.existsSync を使うと、リンク先が既に消えている場合に「無い」と誤判定されます(existsSync はリンクを辿るため)。ここは lstat で見る必要があります。

6-2. 逆に、リンクのせいで「直したのに反映されない」

今度は反対方向の話です。

node_modules をメインへ向けているので、共有パッケージ(@myorg/ui のような workspace パッケージ)の解決先もメインになります。

つまり worktree 側で packages/ui を直しても、dev サーバーも型チェックもユニットテストも、メインリポジトリの古いコードを見続けます。

これ、画面上は「実装が間違っている」ようにしか見えないんですよね。

実際、コンポーネントに入れた補正がまったく効かず、おかしいと思ってハンドラにログを仕込んだら、そのログすら一切出ない。そこでようやく「コードが悪いんじゃなくて、そもそも読まれていないのでは」と気づきました。

疑わしいときは、これを先に叩きます。

node -e "console.log(require('fs').realpathSync('node_modules/@myorg/ui'))"

ポイントは、リポジトリルートと apps/web の両方で確認することです。ルートだけ見て安心すると、下位の node_modules によるシャドーイングを見落とします。

そして厄介なことに、Docker ビルドは通ります。コンテナ内でクリーンにインストールし直すので、この問題だけは検知できません。「CI が通っているから大丈夫」が通用しないケースです。

同じリンクが逆向きに効く例もありました。worktree 側で ORM のクライアント生成を走らせると、生成物がリンク越しにメイン側へ書き込まれます。ブランチのスキーマでメインのクライアントが上書きされ、develop に戻ったときに実在しない型エラーが出ました。

6-3. Claude が worktree を読んで、メインリポジトリに書く

1章で「隔離」をルール化した理由が、これです。

Read は正しく worktree 側のパスを読んでいるのに、続く Write/Edit でパスの先頭(.claude/worktrees/… の部分)が落ちて、メインリポジトリ側の同名ファイルへ書き込む。

しかもツールは成功として返してきます。なので、テストを流すまで気づきません。

おもしろいことに、発生パターンははっきりしていて、同じファイルへの2回目以降の編集で起きます。1回目に正しいパスを使った安心感で、2回目のパスを確認しなくなるからだろうと見ています。ちょっと人間っぽい失敗の仕方だな、と思いました。

対策も単純で、メインリポジトリ側に想定外の差分が出ていないかを、時々 git status で見るだけです。出てしまったら、追跡ファイルは git checkout --、新規ファイルは削除して戻します。

6-4. worktree の「場所」と「起点」は作った直後に確認する

これは小さいですが、地味に時間を溶かしました。

シェルの作業ディレクトリが apps/web のまま、相対パスで worktree を追加して、意図した場所とは違うところにできていたことがあります。

# CWD が apps/web のまま実行すると…
git worktree add .claude/worktrees/<名前>

# ここにできる
apps/web/.claude/worktrees/<名前>

これは絶対パスで作れば起きません。

別の機会には、追加した worktree が最新ブランチではなく Initial commit を起点にした状態になり、当然ながら「あるはずのファイルが無い」と Claude が言い出しました。こちらは再現条件を特定できておらず、切り分けに時間を取られたあげく、該当 worktree を削除して作り直して解決しています。

どちらも、作った直後に1行確認すれば終わる話です。

git worktree list

パスと HEAD が並んで出るので、「変な場所にできていないか」と「起点が想定どおりか」を同時に見られます。

AI が妙なことを言い出したとき、つい「モデルの調子が悪いのかな」と思ってしまいがちなのですが、実際には作業ディレクトリの状態がおかしいだけ、ということがあります。疑う順番を間違えると、けっこう遠回りします。

6-5. gitignore されているファイルは、当然 worktree に無い

最後に、言われてみれば当たり前の話を。

環境変数ファイルはコピーされないので、worktree で dev サーバーを起動すると、起動はするのに全 API が接続情報なしでエラーになります。

画面には何も出ないので、原因にたどり着くまで少しかかりました。worktree で実際にアプリを動かすなら、gitignore されている設定ファイルを手でコピーするところまでが準備、と思っておくとよさそうです。

明日から試すなら

長くなったので、最小構成でまとめます。

  1. CLAUDE.md に「Non-Goals」と「禁止パターン」だけ書く(構成説明は後回しでいい)
  2. 並行させたいタスクが出たら worktree を切る
  3. 切った直後に git worktree list で場所と起点を確認する
  4. 中断するときは状態を文章で残す
  5. 消すときは、リンクを外してから消す。ここだけは急がない

最後に、本数の話を少しだけ。

同時に走らせるのは3本までにしています。それ以上はレビューが追いつかなくなって、結局そこがボトルネックになるからです。

3という数字にも特に根拠はなくて、「2本だと待ちが出る、4本だと自分が破綻する」の間を取っただけです。手を動かすのがAI3体でも、レビューするのは1人なので。

このあたりの適正値はチームによって変わると思います。まずは2本から始めて、レビューが回るかどうかで調整してみるのがちょうどいいかもしれません。

Claude Code をすでに並行で動かしている方にも、これから試してみようという方にも、どこか一箇所でも参考になれば幸いです。