Max Nardit
← 記事
15 mindev

Claude Code の worktree: claude --worktree が分離するものと、ポート、データベース、インストールのように分離しないもの

Claude Code 2.1.316 で検証しました。2 つの worktree が同じポートで衝突し、Docker Compose はそれぞれに別の Postgres ボリュームを計画し、スクリプト実行はロックされたチェックアウトと不要なブランチを残し、デフォルトのベースは未プッシュのコミットを含みませんでした。

目次

Claude Code の worktree のページは、チェックアウトができた後に起きることについて 1 文しか割いていません。「ask Claude to install dependencies, or run your project's setup yourself in the worktree directory」。並列セッションのトラブルの大半は、この 1 文のところにあります。claude --worktree が分けるのはファイルです。開発サーバーがバインドするポート、接続するデータベース、必要なインストールは、共有されたまま、欠けたまま、あるいは重複したままで、このフラグはそのどれにも手を出しません。

使い捨てのリポジトリで、Linux 上の Claude Code 2.1.316 と git 2.43.0 を使い、ローカルの bare リモートに対してこのフラグを試しました。実行はすべて非対話の claude -p で、モデルは Haiku です。実行したものは出力と一緒に載せています。worktree のドキュメントから引いた内容はそう明記し、同じマシン上で何が共有されるかについての自分の推論も同じく明記しています。パスは ~/lab/shop に短縮しています。

Claude Code は —worktree で何を作るのか

リポジトリ内に、新しいブランチ上のリンクされた Git worktree を作り、セッションをその中で開始します。claude --worktree alpha (または -w alpha) は、リポジトリのルートに .claude/worktrees/alpha/ を作り、ブランチ名は worktree-alpha でした。worktree は同じリポジトリの 2 つ目のチェックアウトで、ファイルとインデックスを別に持ちます。詳しくは Codex の worktree についての記事で扱っています。2 つの worktree にいる 2 つのセッションは、別々のファイル群を編集します。約束されているのはそれだけです。

ドキュメントと --help によると、名前を付けなければ Claude Code が bright-running-fox のような名前を自動で付けます。対話実行にはワークスペースの信頼が必要なので、新しいディレクトリでは一度 claude を起動してダイアログを承認します。承認していないと --worktree はそう告げて終了します。claude -p --worktree はこのチェックを飛ばし、今回の実行結果もそのとおりでした。--tmux はセッションを専用の tmux セッションで開き、--worktree が必須です。実行中のセッションで「work in a worktree」と頼むこともでき、その場合 Claude は EnterWorktree ツールで worktree を作ります。

最初の実行の前に .claude/worktrees/ を .gitignore に追加しておきましょう。そうしないと、すべての worktree がメインのチェックアウトに未追跡ファイルとして現れます。

Claude Code の worktree はどのコミットから始まるのか

いま居るコミットではなく、リモートのデフォルトブランチからです。設定は worktree.baseRef で、デフォルトは "fresh"、つまり origin のデフォルトブランチから分岐します。もう 1 つの値 "head" は、現在のローカル HEAD から分岐します。恒久的に設定するには、プロジェクトの .claude/settings.json に書きます。

手元のチェックアウトは feature/discount 上にあり、origin/main より 1 コミット先行していて、web/index.html にコミットしていない変更がありました。プロンプトは stdin から渡したので、--allowedTools=Bash には等号が付いています (理由は後片付けの節で説明します)。

$ echo "Run exactly this one bash command and print its raw output only, nothing else: pwd; git status --short --branch; git log --oneline -3; ls -A" \
    | claude -p --worktree beta --model haiku --allowedTools=Bash
~/lab/shop/.claude/worktrees/beta
## worktree-beta
40e5569 initial
.git
.gitignore
api
web

40e5569 は origin/main です。未プッシュのコミットはなく、worktree には feature ブランチのものが何もありません。次に、もう一方のベースを --settings で渡して同じプロンプトを実行しました。

$ echo "…" | claude -p --worktree gamma --model haiku --allowedTools=Bash --settings '{"worktree":{"baseRef":"head"}}'
## worktree-gamma
ef9e2fd add discount (local, not pushed)
40e5569 initial
ベースworktree の開始点未プッシュのコミット追跡ファイルへの未コミットの変更
"fresh" (デフォルト)origin/mainなしなし
"head"現在のコミットありなし

見落としやすいのは最後の列です。どちらの設定も作業ツリーは引き継ぎません。"head" の worktree の web/index.html は変更前のままでした。worktree はコミットから始まるので、コミットしていない追跡ファイルの変更は置き去りになります。

ドキュメントによると、"fresh" は過去 24 時間にリポジトリが fetch されていなければデフォルトブランチを fetch し、5 秒で諦めてキャッシュ済みの ref を使います。リモートがない場合や、origin/HEAD がキャッシュされておらず取得もできない場合は、ローカルの HEAD にフォールバックします。baseRef にブランチ名は指定できません。特定のブランチから始めたいなら、git worktree add で自分で worktree を作り、その中で claude を起動します。claude --worktree "#1234" なら、origin から fetch したプルリクエストから始まります。

どちらのベースを選ぶかは作業次第です。"fresh" は、自分のブランチで作業を続けながら「main のこのバグを直して」と頼む場合に向いています。"head" は、いま自分がやったことの上に 2 つ目のセッションを積み上げる場合に向いています。その場合は先にコミットしてください。引き継がれるのはコミット済みの作業だけです。

.env と node_modules を Claude Code の worktree に入れるには

リポジトリのルートに .gitignore と同じ構文の .worktreeinclude ファイルを置きます。Claude Code はこれにマッチするファイルを、gitignore されている場合に限ってコピーします。元のチェックアウトには .env と、ダミーのパッケージ left-pad を 1 つだけ入れた node_modules/ ディレクトリがありました。最初の worktree にはどちらも入っていませんでした。そこで次のようにしました。

$ printf '.env\nnode_modules/\n' > .worktreeinclude
$ echo "…ls -A; ls node_modules; cat .env" | claude -p --worktree gamma …
.env
.git
.gitignore
api
node_modules
web
left-pad
SECRET=abc

.worktreeinclude 自体は未追跡で一度もコミットしていませんでしたが、それでも機能しました。そして node_modules/ はリンクではなく別ファイルとしてコピーされました。inode 番号が元のファイルと違っていたことからわかります。このディスクは reflink のない ext4 なので、コピーはそれぞれ実際に容量を使います。node_modules が大きい JavaScript プロジェクトでは worktree ごとにその分を払うことになり、lockfile が元のチェックアウトと違えば、コピーしたインストールはその worktree には合わないかもしれません。依存関係については、worktree の中でインストールを実行するほうが安全なデフォルトです。.worktreeinclude は .env のような小さなファイル向きで、その場合 worktree は存在する間ずっとシークレットのコピーを持つことになります。

Claude Code の worktree 分離は実際に何をブロックするのか

メインのチェックアウトを狙った編集と git コマンドです。判定はパスとコマンドの文字列から行います。ドキュメントの分離の節によると、セッションが worktree に分離されている間、Claude Code はメインのチェックアウトを対象とする Edit、Write、NotebookEdit と、作業ディレクトリがメインのチェックアウトであるコマンドを拒否します。Bash と Monitor では、git -C、--git-dir、GIT_DIR、GIT_WORK_TREE、cd でそこへ向けた git と、文字列から git の使い方を検証できないコマンドも拒否します。PowerShell に適用されるのは作業ディレクトリのチェックだけです。

ドキュメントはこの穴をはっきり書いています。どのチェックもシェルコマンドがどのファイルに書き込むかは追跡しないので、メインのチェックアウトへの cp やシェルのリダイレクトは拒否されません。そこはパーミッションのルールと、使っているならサンドボックスの役目です。

いくつかは意図的に共有されます。これもドキュメントによるものです。worktree からの git の書き込みはメインリポジトリの .git に入るので、中からのコミットは機能します。worktree セッションで Bash コマンドに「Yes, and don't ask again」と答えると、そのルールはメインのチェックアウトの .claude/settings.local.json に保存され、すべての worktree に適用され、承認した worktree を削除しても残ります。フックでは、${CLAUDE_PROJECT_DIR} はセッションを開始したプロジェクトのルートのままで、worktree のパスはフック入力の cwd フィールドで渡されます。このフィールドはその後の cd にも追従します。

つまり Claude Code の分離もファイルレベルです。セッションの編集と git をあなたのチェックアウトから遠ざけます。セッションが起動するものについては何もしません。

並列の Claude Code worktree がそれでも共有するリソースは

リポジトリのファイル以外のすべて、つまりポート、データベース、コンテナ、キャッシュ、API のレート制限です。2 つのセッションがどちらもアプリを起動すると、最初に壊れるのはここです。

いちばん単純なのはポートです。空いているポート 3107 で片方の worktree 用にサーバーを起動し、もう片方でも同じコマンドを実行しました。

$ python3 -m http.server -d .claude/worktrees/beta 3107 &
$ python3 -m http.server -d .claude/worktrees/gamma 3107
OSError: [Errno 98] Address already in use

ポートを固定した開発サーバーも同じように失敗します。--port も PORT も指定しないと、next dev は違う動きをします。Next.js リポジトリの start-server.ts によると、次のポートを最大 10 回試し、元のポートが使用中なので別の空きポートを使う、とログに出します。2 つ目のセッションは、頼んでいないポートで動くサーバーを手にすることになります。エージェントがその警告を読むのか、1 つ目のセッションのものである localhost:3000 に対してテストを続けるのかは、わかりません。

Docker Compose はもっとわかりにくいです。望むかどうかに関係なく、スタックの一部を分離するからです。Compose はデフォルトでプロジェクトにディレクトリ名を付けます。-p、COMPOSE_PROJECT_NAME、トップレベルの name: で別の指定がない限りそうなり、worktree のディレクトリ名は worktree の名前です。5432:5432 を公開し名前付きボリュームを持つ Postgres サービスの同じ compose.yaml を、メインのチェックアウトと 2 つの worktree に置き、Compose に何を作るかを尋ねました (出力は関係するフィールドだけに絞っています)。

shop:  name: shop   published: "5432"  volume: shop_pgdata
beta:  name: beta   published: "5432"  volume: beta_pgdata
gamma: name: gamma  published: "5432"  volume: gamma_pgdata

docker compose config で止め、コンテナは起動していません。それでも計画には両面が表れています。worktree ごとに別のボリューム名が付くので、新しい worktree でデータベースを起動したセッションは新しいボリュームを得ます。初期化スクリプトかセットアップで中身を入れない限り、空のボリュームです。そしてどのプロジェクトも同じホストポートを公開するので、同時に起動できるデータベースは 1 つだけです。Compose は、おそらく共有したかったデータを分離し、分ける必要があったポートを共有します。

ここから先は推論で、検証はしていません。Docker の外、ホスト上で動く Postgres は、接続設定を変えないすべての worktree で共有されるので、2 つのブランチからマイグレーションを流す 2 つのセッションは同じデータベースに書き込みます。~/.npm のようなパッケージキャッシュも共有されますが、これは無害です。外部 API のレート制限も共有され、忘れがちです。同じテストスイートを 1 つのサンドボックスアカウントに対して走らせる 2 つのセッションは、同じクォータを消費します。

どれも --worktree の欠陥ではありません。worktree はファイルレベルの道具で、それを取り巻く調整はエージェントの外に置く必要があります。実際には、マシン全体で共通のものについて worktree ごとに別の値が必要で、コピーされた .env はその置き場所として不適切です。メインのチェックアウトのポートとデータベース名をすべての worktree に持ち込んでしまうからです。検証環境でうまくいった方法は、compose.yaml でポートを変数から読み、worktree ごとの値を、コピーされたファイルを上書きする 2 つ目のファイルに書くことです。

yaml
services:
  db:
    image: postgres:16
    ports:
      - "${PGPORT:-5432}:5432"
    environment:
      POSTGRES_DB: ${PGDATABASE:-shop}
$ printf 'PGPORT=5433\nPGDATABASE=shop_alpha\n' > .env.worktree
$ docker compose --env-file .env --env-file .env.worktree config
        published: "5433"
      POSTGRES_DB: shop_alpha

後に指定した --env-file が優先されます。アプリ側も、自分のポートと接続文字列のために同じ 2 つのファイルを読む必要があります。worktree ごとに空のデータベースを持つより共有のデータベースを 1 つ使いたいなら、ホスト上の 1 つのデータベースか、すべての worktree で共有する外部ボリュームを使い、ブランチ同士がデータを共有することを受け入れてください。

Claude Code のバックグラウンドセッションとサブエージェントは自動で worktree を得るのか

バックグラウンドセッションはたいてい、最初の編集の前に得ます。サブエージェントは頼んだときだけです。Agent view のドキュメントによると、claude --bg で開始したセッションは作業ディレクトリで始まり、ファイルを編集する前に .claude/worktrees/ 配下の worktree へ移ります。すでに開いていたセッションをバックグラウンドに送った場合、セッションや編集対象のファイルがすでにリンクされた worktree にある場合、書き込み先が作業ディレクトリの外の場合、WorktreeCreate フックを設定していない git リポジトリの外の場合は、移動しません。プロジェクト設定の worktree.bgIsolation: "none" でこれを無効にできます。

サブエージェントは、フロントマターに isolation: worktree があるか、Claude に「use worktrees for your agents」と伝えない限り、親のディレクトリを共有します。落とし穴はベースにあります。ドキュメントによると、サブエージェントの worktree は --worktree と同じ worktree.baseRef を使うので、デフォルトの "fresh" のままだと、いま作っている機能のリファクタリングを頼まれたサブエージェントは、未プッシュのコミットを含まない origin/main から始まります。ドキュメントはまさにこのケースに "head" を勧めています。

これもドキュメントによりますが、専用の worktree にいるサブエージェントは、CLAUDE.md と .claude/rules/ を worktree からではなくメイン会話のチェックアウトから読みます。そのブランチに別の内容があってもです。変更のないサブエージェントの worktree は、サブエージェントの終了時に削除されます。残りはバックグラウンドセッションの worktree と一緒に cleanupPeriodDays の経過後に片付けられますが、変更されたファイルも未プッシュのコミットもなくなってからです。

Claude Code の worktree の後片付けと、残るもの

対話モードなら、終了時に Claude Code がほとんどやってくれます。ドキュメントによると、名前のないセッションがクリーンな worktree にあれば worktree とブランチは自動で削除され、名前付きのセッションは先に確認し、変更や新しいコミットのある worktree では必ず確認します。claude -p には終了時の確認がないので worktree は残り、しかもロックされたままです。

$ git worktree remove .claude/worktrees/beta
fatal: cannot remove a locked working tree, lock reason: claude session beta (pid 1153756 start 52741970)
use 'remove -f -f' to override or unlock first

何かを削除する前に、作業を取り出しておきます。worktree-beta は普通のブランチなので、メインのチェックアウトから他のブランチと同じようにマージか cherry-pick をします。そのうえで、ロックを持つセッションが終了していれば、次のようにします。

$ git worktree unlock .claude/worktrees/beta
$ git worktree remove .claude/worktrees/beta
$ git branch -d worktree-beta
Deleted branch worktree-beta (was 40e5569).

3 行目があるのは、git worktree remove がディレクトリを削除しても worktree-beta ブランチは残すからです。worktree を 2 つ手作業で削除した後、ブランチが 2 つ残っていました。未マージのコミットがあるブランチを拒否する -d を使い、-D は本当に捨てるブランチのために取っておきましょう。

実行してみて、目立たない残りものが 2 つ見つかりました。引数の誤りで失敗した claude -p の呼び出しでも、worktree は作られてロックされていました。最初の試行では --allowedTools "Bash" をプロンプトの前に置いたため、可変長のこのフラグがプロンプトを 2 つ目のツール名として取り込み、Claude Code は「Input must be provided either through stdin or as a prompt argument」で終了しました。それでも git worktree list には 40e5569 上の alpha がロックされた状態で表示されました。claude -p --worktree をループで呼ぶスクリプトには、呼び出しが失敗しても走る後片付けの処理が必要です。また、コピーされた .env と node_modules が入った gamma の worktree を削除しても、警告はまったく出ませんでした。git は変更されたファイルや未追跡ファイルのある worktree の削除を拒否しますが、無視されたファイルはディレクトリと一緒に削除されます。エージェントがビルドや出力用のフォルダのような無視されたパスに書いたものも含めてです。

ドキュメントによると、後のセッションで古いロックの掃除が走り、プロセスが終了したロックは解放されます。ロックを自分で確認するなら、git worktree list --verbose が各ロックの理由を PID 付きで表示します。

$ git worktree list --verbose
~/lab/shop                          ef9e2fd [feature/discount]
~/lab/shop/.claude/worktrees/alpha  40e5569 [worktree-alpha]
	locked: claude session alpha (pid 1153520 start 52741354)

1 つのリポジトリで 2 つ目の Claude Code セッションを動かす前に準備すること

5 つあり、後半の 3 つは worktree が手を付けない部分です。.claude/worktrees/ を .gitignore に追加します。ベースを決めます。main から始める作業には "fresh" のまま、2 つ目のセッションやサブエージェントが自分のブランチの上に積む必要があるなら、必要なものをコミットしたうえで "head" にします。.env などの小さな無視ファイルは .worktreeinclude に入れ、依存関係はコピーせず worktree の中でインストールします。2 つ目の開発サーバーやデータベースを起動する前に、コピーされた .env を上書きするファイルで、worktree ごとにホストポートとデータベース名を割り当てます。そして claude -p を実行するたびに、残したいものをマージしてから、unlock、remove、git branch -d をします。そうしないと、ロックされたチェックアウトと worktree-* ブランチがたまっていきます。

ブランチが前もって決まっているなら、今でも手作業のほうが短く済みます。git worktree add -b fix/login ../shop-login origin/main はブランチ名と開始点を 1 コマンドで指定でき、その中で起動した claude はそこで作業します。Codex CLI の管理型 worktree でも結論は同じでした。管理型のフラグが役に立つのは、チェックアウトが使い捨てで、後片付けを任せたい場合です。ドキュメントによれば後片付けは対話モードの終了時に行われますが、スクリプト実行では一切行われないことが今回の検証で確認できました。

01 / ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit@mnardit

02 / ほかの記事

  1. Claude Ultra が Max 20x に代わる: 認証済みアカウントは 5 時間制限なし、Anthropic によれば週の上限は最初から Pro の 10x

    旧名の 20x が表していたのは 5 時間枠で、2 つあるメーターのうち緩いほうでした。Anthropic の説明では、週の上限は導入された日から Pro の 10x です。Ultra の料金表示は週の数字を前面に出し、5 時間の数字をなくしました。人によっては条件が良くなり、20x を週の量と読んでいた人にとっては、読み違いだったことになります。

  2. コーディングエージェントへのプロンプトインジェクション事例:テキストはどこから入り、何がそれを止めたのか(止めたものがあったとすれば)

    公開された 9 件の事例を、攻撃者の言葉が入ってきた入口ごとに分類しました。ほとんどの事例で、攻撃の最中に持ちこたえたものはありません。その後の修正はすべて同じ種類でした。決定論的なチェックを向け直すか、機能を取り除くかです。失敗したのがモデルの判断だった場合、向け直す対象はありませんでした。

  3. バイブコーディングとエージェンティック・エンジニアリングの違い: セッションが終わった後もコードを検査し続けるもの

    Simon Willison は、コードをレビューし、それを説明できる人がいるかどうかで線を引きます。ただ、そのレビューは一度きりで、チャットが閉じた後もコードは変わり続けます。エージェンティック・エンジニアリングが名前を言い換えただけのものでないなら、違いは、誰も見ていないときに動き、しかも意図した理由で失敗するのを確かめたチェックにあります。