本文へ移動
購読
GUIDE#devtools更新 2026.10.02

git worktreeの使い方:複数ブランチを並行作業する手順とつまずき対処

git worktreeで複数ブランチを同時に開く手順を実行結果つきで。.gitファイルの仕組み、実際に出したエラーの直し方、cloneとの容量比較、AIエージェント並列用の配置ルールまで。

level
中級
time
約30分
stack
Ubuntu 26.04.1, Git 2.53.0
author
@ryoma_t
// 前提条件
git commit・branch・pushの基本操作ができること / 壊してもよい練習用の環境があること
git worktree addで作業ツリーを追加し、.gitファイルの中身と使用中ブランチのエラーを確認したターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

git worktreeとは、1つのリポジトリの履歴(.gitの中身)を共有したまま、別のブランチを別のディレクトリに同時にチェックアウトできるGit標準のサブコマンドです。リポジトリをもう1つcloneするのとは違い、オブジェクトもブランチも1か所にあるので、片方でコミットした内容はfetchなしでもう片方から見えます。

mainで障害対応をしながら機能ブランチの作業を止めたくない人や、Claude CodeなどのAIコーディングエージェントを複数同時に走らせたい人向けの手順です。コマンドと出力はすべてUbuntu 26.04.1とGit 2.53.0の使い捨てコンテナで実行したもので、題材には公開リポジトリのjqとcurlを使いました。最後まで進めると、エラーの原因を.gitファイルの中身から説明でき、エージェントごとの作業場所を1コマンドで作って片付けられるようになります。

この記事でわかること

  • .gitファイルと.git/worktrees/による仕組みと、worktree間で共有されるもの・されないもの
  • git worktree add・list・remove・prune・repairの使い方
  • 「already used by worktree」「contains modified or untracked files」など実際に出したエラーと対処
  • curlリポジトリで測った、worktree追加と2つ目のcloneのディスク使用量の差
  • AIエージェントを並列で動かすためのディレクトリ配置ルールと、作成・片付け用のスクリプト

準備:Gitと練習用リポジトリ

Ubuntu 26.04.1のaptで入るGitは執筆時点で2.53.0でした。worktree自体はずっと前からある機能ですが、後半で使う--relative-pathsは古いGitにはないため、先にバージョンを確認しておきます。コミットに使う名前とメールアドレスも設定します(下のRyoma Tと[email protected]は検証用の値なので、自分の名前とメールアドレスに置き換えてください。出力に出てくる/home/ryomaも検証機のホームディレクトリです)。

terminal
sudo DEBIAN_FRONTEND=noninteractive apt install -y git
git --version
git version 2.53.0
git config --global user.name "Ryoma T" && git config --global user.email [email protected]

練習用に、手頃な大きさの公開リポジトリとしてjqをcloneします。既定ブランチはmainではなくmasterです。以降の出力に出てくる9a75bb0は執筆時点の先頭コミットで、手元で実行するとハッシュは変わります。

terminal
mkdir -p ~/work && cd ~/work && git clone https://github.com/jqlang/jq.git
Cloning into 'jq'...
cd jq

git worktreeの仕組み:.gitファイルとgitdirのつながり

最初のworktreeを作ります。置き場所はリポジトリの中ではなく、隣に作ったjq.wt/ディレクトリにします(理由は後半の配置ルールで実測します)。-b fix-docsで新しいブランチを作り、それを../jq.wt/fix-docsにチェックアウトしています。

terminal
git worktree add -b fix-docs ../jq.wt/fix-docs
Preparing worktree (new branch 'fix-docs')
HEAD is now at 9a75bb0 Improve performance of join/1 on large arrays (#3639)
git worktree list
/home/ryoma/work/jq             9a75bb0 [master]
/home/ryoma/work/jq.wt/fix-docs 9a75bb0 [fix-docs]

追加したディレクトリの.gitは、ディレクトリではなく52バイトのテキストファイルです。中身は1行だけで、本体リポジトリの中にある管理ディレクトリの場所を指しています。

terminal(抜粋)
ls -la ../jq.wt/fix-docs | head -8
-rw-r--r--  1 ryoma ryoma     52 Sep 29 07:52 .git
cat ../jq.wt/fix-docs/.git
gitdir: /home/ryoma/work/jq/.git/worktrees/fix-docs

指された先の.git/worktrees/fix-docs/には、このworktree専用のファイルが入っています。HEAD(いまどのブランチにいるか)とindex(ステージした内容)がworktreeごとに別なので、2つのディレクトリで別々のブランチを開き、別々にgit addできます。gitdirは逆向きのリンクでworktree側の.gitファイルの場所、commondirは共有部分(本体の.git)への相対パスです。

terminal
ls .git/worktrees/fix-docs
HEAD
ORIG_HEAD
commondir
gitdir
index
logs
refs
cat .git/worktrees/fix-docs/gitdir .git/worktrees/fix-docs/commondir .git/worktrees/fix-docs/HEAD
/home/ryoma/work/jq.wt/fix-docs/.git
../..
ref: refs/heads/fix-docs

Gitが実際にどこを見ているかはgit rev-parseで確かめられます。worktree側では--git-dir(専用部分)と--git-common-dir(共有部分)が別の場所を返し、本体側ではどちらも.gitです。後のつまずきポイントで出てくる「not a git repository」は、この往復リンクのどちらかが切れた状態です。

terminal(worktree側で実行)
git rev-parse --git-dir --git-common-dir
/home/ryoma/work/jq/.git/worktrees/fix-docs
/home/ryoma/work/jq/.git

共有部分には、オブジェクト(コミットやファイルの中身)とrefs/以下のブランチ・タグがあります。そのため、worktree側でコミットすると、本体側から同じコミットがすぐ見えます。pushもfetchも挟んでいません。

terminal(fix-docs側でコミットし、本体側で確認)
echo "worktree test" >> README.md && git commit -qam "docs: worktree test" && git log --oneline -1
72f47e8 docs: worktree test
git log --oneline -1 fix-docs
72f47e8 docs: worktree test
対象worktreeごとに別全worktreeで共有
HEAD(今いるブランチ)別―
index(ステージした内容)別―
作業ファイル・未追跡ファイル(.envなど)別―
ブランチ・タグ(refs/heads、refs/tags)―共有
stash(refs/stash)―共有
オブジェクト・リモート設定・.git/config―共有

表のうちstashの共有は見落としやすいので、実際に確かめました。fix-docs側でgit stash -uした内容が、本体側のgit stash listにそのまま出ます。

terminal(fix-docs側でstash、本体側で一覧)
echo tmp > note.txt && git stash -u && git stash list
Saved working directory and index state WIP on fix-docs: 72f47e8 docs: worktree test
stash@{0}: WIP on fix-docs: 72f47e8 docs: worktree test
git stash list
stash@{0}: WIP on fix-docs: 72f47e8 docs: worktree test

stashは全worktreeで1本のスタックです。別のworktreeでgit stash popすると、他人(他のエージェント)が積んだ変更を自分のブランチに展開してしまいます。worktreeを使うなら、一時退避はstashではなくそのworktreeのブランチにWIPコミットを積むほうが安全です。今回はfix-docs側でgit stash popして戻しました。

git worktreeの使い方:add・list・removeの基本形

日常で使う形は3つに絞れます。どれも本体(~/work/jq)で実行する前提です。

やりたいことコマンド補足
新しいブランチで作業を始めるgit worktree add -b 新ブランチ 置き場所 起点起点を省くと今のHEAD。リモートの最新から始めたいならorigin/masterを指定
レビューや動作確認だけしたいgit worktree add --detach 置き場所 コミットブランチを作らないので、他のworktreeが使用中のブランチでも開ける
終わったので片付けるgit worktree remove 置き場所未コミットの変更や未追跡ファイルがあると拒否される(安全装置)

--detachはブランチではなくコミットを直接チェックアウトします。masterは本体が使用中なので普通は2か所目に開けませんが(次の節のエラー)、detached HEADなら開けます。PRのレビューやビルド確認に向いています。

terminal
git worktree add --detach ../jq.wt/review master
Preparing worktree (detached HEAD 9a75bb0)
HEAD is now at 9a75bb0 Improve performance of join/1 on large arrays (#3639)
git worktree list
/home/ryoma/src/jq           9a75bb0 [master]
/home/ryoma/src/jq.wt/review 9a75bb0 (detached HEAD)
git worktree remove ../jq.wt/review

上の--detachの出力は、この記事の練習をすべて終えて~/srcに置き直し、ほかのworktreeを片付けた後に取ったため、パスが~/srcで、一覧も2行だけです。途中で試すと、その時点のworktreeも並びます。サブコマンドの一覧はgit worktree -hで確認でき、Git 2.53.0ではadd・list・lock・move・prune・remove・repair・unlockの8つでした。

jqにはサブモジュール(vendor/oniguruma)があります。git worktree addはサブモジュールを展開しないため、新しいworktreeでgit submodule statusを実行すると先頭に-(未初期化)が付いた行が出ます。ビルドが必要なリポジトリでは、worktreeを作るたびにgit submodule update --initを実行してください。

terminal(fix-docs側で実行)
git submodule status
-4ef89209a239c1aea328cf13c05a2807e5c146d1 vendor/oniguruma

つまずきポイント:実際に出したエラーと対処

fatal: 'master' is already used by worktree at …

Gitは、同じブランチを2つのworktreeで同時にチェックアウトすることを拒否します。両方で別々にコミットすると、どちらのHEADが正しいのか決められなくなるためです。masterを2か所目に開こうとしたとき、本体でfix-docsに切り替えようとしたとき、使用中のブランチを削除しようとしたときに、それぞれ次のように止まります。

terminal(エラー)
git worktree add ../jq.wt/hotfix master
Preparing worktree (checking out 'master')
fatal: 'master' is already used by worktree at '/home/ryoma/work/jq'
git switch fix-docs
fatal: 'fix-docs' is already used by worktree at '/home/ryoma/work/jq.wt/fix-docs'
git branch -d fix-docs
error: cannot delete branch 'fix-docs' used by worktree at '/home/ryoma/work/jq.wt/fix-docs'

対処は目的で分けます。作業するなら-bで新しいブランチを切る、見るだけなら--detach(前の節の表)、ブランチを消したいなら先にそのworktreeをremoveします。失敗したaddは../jq.wt/hotfixを作らずに終わっていたので(直後のls ../jq.wtはfix-docsだけ)、同じ場所に新しいブランチで作り直せます。

起点にorigin/masterを指定すると、作ったブランチの上流(upstream)も自動でorigin/masterに設定されます。ローカルのmasterが古いままでも、リモートの最新から枝分かれできます。

terminal
git worktree add -b hotfix ../jq.wt/hotfix origin/master
Preparing worktree (new branch 'hotfix')
branch 'hotfix' set up to track 'origin/master'.
HEAD is now at 9a75bb0 Improve performance of join/1 on large arrays (#3639)

fatal: … contains modified or untracked files, use --force to delete it

git worktree removeは、未コミットの変更や未追跡ファイルが残っていると削除しません。stashの確認で作ったnote.txtがfix-docsに残っていたため、次のように拒否されました。

terminal(エラー)
git worktree remove ../jq.wt/fix-docs
fatal: '../jq.wt/fix-docs' contains modified or untracked files, use --force to delete it

--forceを付ける前に、何が残っているかを必ず見ます。git -Cで本体から覗けます。ここではnote.txtだけで、消してよいものです。ただしfix-docsは後の移動テストで使うので、実際に--forceで消すのはAIエージェントの節の冒頭で行います。

terminal
git -C ../jq.wt/fix-docs status --short
?? note.txt

rm -rfで消したworktreeが「prunable」として残る

worktreeのディレクトリをrm -rfで消しても、本体側の.git/worktrees/hotfix/は残ります。git worktree listにはprunable付きで表示され、同じ場所に作り直そうとすると止まります。

terminal(エラー)
rm -rf ../jq.wt/hotfix
git worktree list
/home/ryoma/work/jq             9a75bb0 [master]
/home/ryoma/work/jq.wt/fix-docs 72f47e8 [fix-docs]
/home/ryoma/work/jq.wt/hotfix   9a75bb0 [hotfix] prunable
git worktree add ../jq.wt/hotfix hotfix
Preparing worktree (checking out 'hotfix')
fatal: '../jq.wt/hotfix' is a missing but already registered worktree;
use 'add -f' to override, or 'prune' or 'remove' to clear

git worktree prune -vで、実体のない管理ディレクトリを掃除します。-vを付けると何を消したか表示されます。ブランチhotfixは消えずに残るので、不要なら別途git branch -dします。

terminal
git worktree prune -v
Removing worktrees/hotfix: gitdir file points to non-existent location
git worktree list
/home/ryoma/work/jq             9a75bb0 [master]
/home/ryoma/work/jq.wt/fix-docs 72f47e8 [fix-docs]
git branch --list hotfix
  hotfix

ディレクトリを移動したら fatal: not a git repository

既定では、往復リンクはどちらも絶対パスで書かれます。そのため、~/workごと~/srcへ移動すると、worktree側の.gitファイルが指す移動前の絶対パス(例では/home/ryoma/work/…)が存在しなくなり、worktree内のGitコマンドがすべて失敗します。本体側からは、逆向きのリンクが切れたworktreeがprunableに見えます。

terminal(エラー)
mv ~/work ~/src
git status
fatal: not a git repository: /home/ryoma/work/jq/.git/worktrees/fix-docs
git worktree list
/home/ryoma/src/jq              9a75bb0 [master]
/home/ryoma/work/jq.wt/fix-docs 72f47e8 [fix-docs] prunable

ここでpruneを実行してはいけません。管理ディレクトリが消え、worktreeの作業内容は残っていても、Gitからはつながらなくなります。直すのはgit worktree repairで、本体から移動後のworktreeのパスを渡すと、両方向のリンクを書き直します。

terminal
cd ~/src/jq
git worktree repair ../jq.wt/fix-docs
repair: gitdir incorrect: /home/ryoma/src/jq/.git/worktrees/fix-docs/gitdir
git worktree list
/home/ryoma/src/jq             9a75bb0 [master]
/home/ryoma/src/jq.wt/fix-docs 72f47e8 [fix-docs]
cat .git
gitdir: /home/ryoma/src/jq/.git/worktrees/fix-docs

git statusと最後のcat .gitはfix-docs側(~/src/jq.wt/fix-docs、移動前は~/work/jq.wt/fix-docs)で、それ以外は本体で実行しています。gitdir:の行が新しい~/srcのパスに書き換わっていれば修復済みです。

git gcも、実体が見つからないworktreeの管理ディレクトリを一定期間後に自動で削除します(期間はgc.worktreePruneExpireで決まります)。外付けディスクやネットワーク共有に置いたworktree、エージェントが作業中のworktreeはgit worktree lock --reason "理由"でロックしておくと、prune・remove・moveの対象から外れます。ロック中にremoveすると「cannot remove a locked working tree, lock reason: …」で止まります。

--relative-pathsで移動やコンテナのマウントに強くする

Git 2.53.0のgit worktree addには--relative-pathsがあり、往復リンクを相対パスで書きます。本体と.wtディレクトリの位置関係さえ保てば、親ディレクトリごと移動しても、別のパスにマウントしても壊れません。

terminal
git worktree add --relative-paths -b agent-1 ../jq.wt/agent-1 origin/master
Preparing worktree (new branch 'agent-1')
branch 'agent-1' set up to track 'origin/master'.
HEAD is now at 9a75bb0 Improve performance of join/1 on large arrays (#3639)
cat ../jq.wt/agent-1/.git .git/worktrees/agent-1/gitdir
gitdir: ../../jq/.git/worktrees/agent-1
../../../../jq.wt/agent-1/.git

効果を比べるため、絶対パスのfix-docsと相対パスのagent-1を並べたまま、~/srcを~/movedへ移動しました。agent-1は移動後もそのまま動き、fix-docsは先ほどと同じエラーになります。確認後は~/srcに戻しています。

terminal(移動後の比較・fix-docs はエラー)
mv ~/src ~/moved
git -C ~/moved/jq.wt/agent-1 status --short --branch
## agent-1...origin/master
git -C ~/moved/jq.wt/fix-docs status --short --branch
fatal: not a git repository: /home/ryoma/src/jq/.git/worktrees/fix-docs
mv ~/moved ~/src

代償は互換性です。このオプションを使うとリポジトリの設定にextensions.relativeWorktreesが書き込まれ、この拡張を知らない古いGitではリポジトリを扱えなくなります。IDEに同梱された古いGitや、古いディストリビューションのCIイメージが同じディレクトリを触る環境では、絶対パスのまま使い、移動したらrepairする運用のほうが無難です。

terminal
git config --get extensions.relativeWorktrees; git config --get core.repositoryformatversion
true
1

ディスク使用量:worktree追加と2つ目のcloneを実測

worktreeは作業ファイルだけを増やし、オブジェクトは本体の.gitを共有します。どれくらい差が出るかを、執筆時点のcurlリポジトリ(先頭コミット657d7f18cf、2026-09-29)で測りました。cloneしてworktreeを1つ足し、比較用にもう1つcloneしています。

terminal
cd ~/src
git clone -q https://github.com/curl/curl.git && git -C curl worktree add -q ../curl.wt/agent-1 -b agent-1 && git clone -q https://github.com/curl/curl.git curl-clone2
du -sh curl/.git
134M	curl/.git
du -sh curl.wt/agent-1
30M	curl.wt/agent-1
du -sh curl/.git/worktrees/agent-1
436K	curl/.git/worktrees/agent-1
du -sh curl-clone2
163M	curl-clone2
増やし方増えるものcurlでの実測jqでの実測
worktreeを1つ追加作業ファイル+本体側の管理ディレクトリ30M+436K6.1M+76K
2つ目をリモートからclone作業ファイル+.git全体163M15M(うち.gitは8.7M)

curlでは、2つ目のcloneがworktree 1つの約5倍の容量を使いました。差のほとんどは.git(134M)で、履歴が長いリポジトリほど開きます。エージェントを3つ並べるなら、worktreeなら作業ファイル3つ分、cloneなら.gitも3つ分です。容量だけでなく、cloneは3つそれぞれでfetchしないと他のcloneのコミットが見えない点も、並列作業では手間になります。jqの数値は次のコマンドで取りました。

terminal
du -sh jq/.git
8.9M	jq/.git
du -sh jq.wt/agent-1
6.1M	jq.wt/agent-1
du -sh jq/.git/worktrees/agent-1
76K	jq/.git/worktrees/agent-1
git clone -q https://github.com/jqlang/jq.git jq-clone2 && du -sh jq-clone2 jq-clone2/.git
15M	jq-clone2
du -sh jq-clone2/.git
8.7M	jq-clone2/.git

手元のリポジトリからcloneすると(git clone ~/src/jq jq-local)、Gitはオブジェクトをハードリンクで共有するため、.gitは244Kしか増えませんでした。ただしこのcloneのoriginはGitHubではなくclone元のローカルパス(例では/home/ryoma/src/jq)を指します。pushしても本体リポジトリに届くだけで、ブランチの見え方もfetch次第です。容量を抑えたいだけなら、ローカルcloneよりworktreeのほうが扱いが素直です。

terminal
git clone -q ~/src/jq jq-local && du -sh jq/.git jq-local/.git
8.9M	jq/.git
244K	jq-local/.git
git -C jq-local remote get-url origin
/home/ryoma/src/jq

worktreeで節約できるのは.gitの分だけです。node_modulesやbuild/、Pythonの仮想環境のようなビルド成果物はworktreeごとに作られるので、依存の重いプロジェクトでは作業ファイルより成果物のほうが大きくなります。エージェントを増やす前に、1つのworktreeでビルドまで済ませた状態をdu -shで測ってから台数を決めてください。

AIエージェントを並列で動かすときの配置ルール

Claude Codeのようなコーディングエージェントを同じリポジトリで2つ以上動かすと、同じ作業ディレクトリのファイルを互いに書き換え、片方のテストがもう片方の途中の変更で落ちます。worktreeで作業ディレクトリとindexを分ければこの衝突は起きません。ここでは、実際に試して決めた配置ルールを示します。

配置例
~/src/
├── jq/                 本体(master)。エージェントは入れない。fetch とレビュー用
└── jq.wt/              worktree 置き場(本体の外、隣)
    ├── login-form/     ブランチ agent/login-form  ← エージェントA
    └── api-timeout/    ブランチ agent/api-timeout ← エージェントB
  • worktreeは本体の外、隣の<リポジトリ名>.wt/に置く。本体の中(例:wt/)に作ると、本体のgit statusに?? wt/と出ました。.gitignoreで隠せても、本体で動くgrepやテスト、エディタのインデックスが他のエージェントの作業ファイルまで拾います。
  • 1エージェント=1worktree=1ブランチ。ブランチ名はagent/<タスク名>、ディレクトリ名は<タスク名>とそろえると、git worktree listだけで誰がどこで何をしているか読めます。
  • 起点はローカルのmasterではなくoriginの既定ブランチ。本体のmasterを更新し忘れていても、fetch直後のリモートから枝分かれします。
  • エージェントが動いている間は--lockしておく。別の作業でgit worktree pruneやremoveを打っても消えません。
  • 未追跡ファイルはコピーされない前提で用意する。本体に置いた.envは新しいworktreeにはありません(ls: cannot access '../jq.wt/agent-1/.env': No such file or directory)。複数のエージェントが開発サーバーを立てるなら、.envのポート番号もworktreeごとに変えます。
  • コンテナやサンドボックスにマウントして動かすなら--relative-paths。本体と.wtを同じ親ディレクトリごとマウントすれば、コンテナ内のパスが違っても.gitファイルがたどれます(マウントでの動作はこの環境では試しておらず、上の移動テストで相対リンクが保たれることだけを確認しています)。
terminal(本体の中に作った場合)
git worktree add -q -b nested-test wt/nested-test origin/master && git status --short
?? wt/
git worktree remove wt/nested-test && git branch -D nested-test
Deleted branch nested-test (was 9a75bb0).

スクリプトに入る前に、エージェントが動いている状態をagent-1で再現しておきます。ロックするとlist --verboseに理由が表示され、pruneの対象から外れ、removeは拒否されます。

terminal(ロック中の remove はエラー)
cd ~/src/jq
git worktree lock --reason "agent-1 running" ../jq.wt/agent-1
git worktree list --verbose
/home/ryoma/src/jq             9a75bb0 [master]
/home/ryoma/src/jq.wt/agent-1  9a75bb0 [agent-1]
	locked: agent-1 running
/home/ryoma/src/jq.wt/fix-docs 72f47e8 [fix-docs]
git worktree prune -v
git worktree remove ../jq.wt/agent-1
fatal: cannot remove a locked working tree, lock reason: agent-1 running
use 'remove -f -f' to override or unlock first

ここまでの練習用worktreeを片付けます。fix-docsは未追跡のnote.txtを確認済みなので--forceで、agent-1はロックを外してから通常のremoveで消します。

terminal
git worktree remove --force ../jq.wt/fix-docs && git branch -D fix-docs
Deleted branch fix-docs (was 72f47e8).
git worktree unlock ../jq.wt/agent-1 && git worktree remove ../jq.wt/agent-1 && git branch -d agent-1
Deleted branch agent-1 (was 9a75bb0).

このルールを毎回手で守るのは面倒なので、作成と片付けをスクリプトにしました。newでfetchしてからロック付きのworktreeを作り、本体の.envをコピーします。doneはロックを外してremoveし、ブランチを-d(マージ済みのときだけ削除)で消します。本体でも、どのworktreeの中からでも実行できるよう、本体の場所は--git-common-dirから求めています。

~/bin/wt-agent
#!/usr/bin/env bash
# wt-agent new <task>  : origin の既定ブランチから agent/<task> を作り、<repo>.wt/<task> に展開
# wt-agent done <task> : 変更が残っていなければ worktree とブランチを片付ける
set -euo pipefail
cmd="${1:?new|done}"; task="${2:?task name}"
main="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"
dir="$(dirname "$main")/$(basename "$main").wt/${task}"
branch="agent/${task}"

case "$cmd" in
  new)
    git -C "$main" fetch -q origin
    base="$(git -C "$main" symbolic-ref --short refs/remotes/origin/HEAD)"
    git -C "$main" worktree add -q --relative-paths --lock --reason "agent ${task}" \
      -b "$branch" "$dir" "$base"
    if [ -f "$main/.env" ]; then cp "$main/.env" "$dir/.env"; fi
    echo "$dir"
    ;;
  done)
    # new でコピーした .env が未追跡ファイルとして残ると remove が拒否するので、同一なら消す
    if [ -f "$dir/.env" ] && cmp -s "$main/.env" "$dir/.env"; then rm "$dir/.env"; fi
    git -C "$main" worktree unlock "$dir" 2>/dev/null || true
    if ! git -C "$main" worktree remove "$dir"; then
      git -C "$main" worktree lock --reason "agent ${task}" "$dir"
      exit 1
    fi
    git -C "$main" branch -d "$branch"
    ;;
  *) echo "usage: wt-agent new|done <task>" >&2; exit 2 ;;
esac

最初に書いた版のdoneには.envを消す行がなく、コピーした.envが未追跡ファイルとして残るせいでremoveが「contains modified or untracked files」で止まりました。本体と中身が同じときだけ消すcmp -sの行はその対策です。エージェントが.envを書き換えていれば消さずに止まるので、変更を失いません。

上の内容を~/bin/wt-agentとして保存し(mkdir -p ~/binで置き場所を作ってから)、実行権を付けます。.envのコピーを確かめるため、本体に練習用の.envも置きます。jqの.gitignoreは.envを無視しないので、本体のgit statusには未追跡として出ます。

terminal
mkdir -p ~/bin
echo "PORT=8080" > .env && git status --short --ignored
?? .env
chmod +x ~/bin/wt-agent
~/bin/wt-agent new login-form && ~/bin/wt-agent new api-timeout
/home/ryoma/src/jq.wt/login-form
/home/ryoma/src/jq.wt/api-timeout
git worktree list --verbose
/home/ryoma/src/jq                9a75bb0 [master]
/home/ryoma/src/jq.wt/api-timeout 9a75bb0 [agent/api-timeout]
	locked: agent api-timeout
/home/ryoma/src/jq.wt/login-form  9a75bb0 [agent/login-form]
	locked: agent login-form
cat ../jq.wt/login-form/.git && cat ../jq.wt/login-form/.env
gitdir: ../../jq/.git/worktrees/login-form
PORT=8080

login-form側でエージェントがコミットした想定で1コミット積み、2つとも片付けます。何もしなかったapi-timeoutはブランチごと消え、コミットのあるlogin-formはworktreeだけ消えてブランチが残ります。

terminal(done の結果・login-form はエラー)
echo "/* login */" >> src/main.c && git commit -qam "wip: login form"
~/bin/wt-agent done api-timeout
Deleted branch agent/api-timeout (was 9a75bb0).
~/bin/wt-agent done login-form
error: the branch 'agent/login-form' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D agent/login-form'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
git worktree list && git branch --list "agent/*"
/home/ryoma/src/jq 9a75bb0 [master]
  agent/login-form

not fully mergedはここでは意図どおりの結果で、レビュー前のコミットをブランチとして残すための止まり方です。PRを出すならgit push -u origin agent/login-form、捨てるならgit branch -D agent/login-formで片付けます。放置したまま同じタスク名でnewすると、fatal: a branch named 'agent/login-form' already existsで止まります。

エージェントごとにworktreeを分けても、リモートとブランチ名の空間は共有です。2つのエージェントに同じタスク名を与えると2つ目のnewが上のエラーで止まり、同じファイルを触るタスクを並べると衝突はmerge時に先送りされるだけです。タスクは触るディレクトリが重ならない単位で切り、並べる数は人がレビューしきれる数に抑えてください。

動作確認

worktreeが正しく作れているかは、次の2点で判断できます。git worktree listに並ぶ行にprunableが付いていないこと、worktreeの中でgit rev-parse --git-dir --git-common-dirが本体の.git配下の2つのパスを返すことです。本体側では両方とも.gitになります。

terminal(本体で実行)
git rev-parse --git-dir --git-common-dir
.git
.git

すべて片付けた後は、git worktree listが本体の1行だけになり、.wtディレクトリは空、本体の.git/worktreesは最後のworktreeを消した時点でディレクトリごとなくなります。

terminal
git worktree list; ls -A ../jq.wt; test -d .git/worktrees || echo "no .git/worktrees"
/home/ryoma/src/jq 9a75bb0 [master]
no .git/worktrees

よくある質問

git worktreeを削除するには?

本体でgit worktree remove <パス>を実行します。未コミットの変更や未追跡ファイルがあると拒否されるので、git -C <パス> status --shortで中身を見てから、不要な場合だけ--forceを付けます。rm -rfで消してしまった場合はgit worktree prune -vで管理情報を掃除し、ブランチはgit branch -dで別に消します。

git worktreeとgit cloneの違いは?

worktreeは1つの.gitを共有し、作業ディレクトリだけを増やします。curlでの実測では、worktree 1つが30M、2つ目のcloneが163Mでした。コミットやブランチも即座に共有されます。一方、同じブランチを2か所で開けない制約があり、stashも共有です。完全に独立した環境が欲しいときはcloneを選びます。

git worktreeはVS Codeで使える?

worktreeのディレクトリは.gitファイルを持つ普通の作業ディレクトリなので、そのフォルダをVS Codeの別ウィンドウで開けば、Git連携はそのworktreeのブランチとして動きます。VS Code側のworktree専用の画面や操作はバージョンで変わるため、この記事では扱わず、この環境でも試していません。本体とworktreeを同じワークスペースに入れると検索対象が重複する点に注意してください。

AIエージェントを並列で動かすときにworktreeを使う理由は?

エージェントごとに作業ファイルとindexを分けられ、互いの途中の変更でビルドやテストが壊れなくなるからです。cloneと違い.gitを共有するので、容量はほぼ作業ファイル分で済み、各エージェントのコミットはfetchなしで本体からレビューできます。ロックとagent/<タスク名>の命名で、誰の作業場所かもgit worktree listで追えます。

まとめ

worktreeの実体は、作業ディレクトリの.gitファイルと本体の.git/worktrees/<名前>/の往復リンクです。エラーの多くはこのリンクか「1ブランチは1か所だけ」の制約から来ており、-b・--detach・prune・repairで直せます。並列作業では本体の隣に.wt/を作り、1エージェント1ブランチ、作業中はロックの形にそろえます。次の作業として、自分のリポジトリでwt-agent newを1回実行し、ビルドまで済ませたworktreeをdu -shで測って、並べられる数を決めてください。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。