Claude Code・Codex・Cursorなどを並列実行する前に、Git worktreeが分離する範囲を5つの統合テストで確認します。.env欠落、同一行競合、dirty削除拒否を再現し、fail-closedの検査スクリプトを導入できます。
AIエージェントをGit worktreeで並列化する前に|分離できない5項目を実測
Git worktreeを使えば、Claude Code・Codex・Cursorなど複数のAIエージェントへ別々の作業ディレクトリを渡せます。ただし分離される中心はチェックアウトされたファイルです。Git履歴、remote、マージ先、ポート、DB、秘密ファイルまで別環境になるわけではありません。
この記事では、2つのworktreeを一時リポジトリへ作り、編集分離、gitignoredな.envの欠落、別ファイルの正常マージ、同一行の競合、dirty worktreeの削除拒否を5件の統合テストで再現します。さらに、全worktreeがcleanな場合だけ完全無出力で終了するworktree_guard.pyをコピーできる形で示します。
検証日は2026年7月22日です。実行環境はmacOS 26.5.2 arm64、Apple Git 2.50.1、Python 3.9.6でした。端末に存在したClaude Codeは2.1.209、Codex CLIは0.144.2ですが、今回の実行検証は各製品の自動worktree機能ではなく、共通基盤であるGitコマンドの境界に限定しています。
Git公式のgit-worktree文書は、1つのリポジトリへ複数のworking treeを関連付けられると説明しています。一方、AI並列開発で重要なのは作成方法よりも、何が共有されたままかです。別ディレクトリで動いているという見た目だけで完全分離と判断すると、次の事故が残ります。
main側にだけある.envが新worktreeになく、エージェントのテストだけ失敗する。
別ブランチでも同じ行、lockfile、schema、共通設定を変更し、統合時に競合する。
同じポート、DB、キャッシュ、テストアカウントを使い、実行時に干渉する。
未追跡の生成物が残ったworktreeを強制削除し、成果を失う。
検査ログをworktree内へ出力し、そのログ自体がdirty判定を起こす。
完成状態は、並列実行前にpython3 worktree_guard.py /path/to/repoを実行し、cleanなら終了コード0かつstdout・stderrとも0 byte、問題があれば終了コード2かつstderrへ対象パスを出す状態です。終了コード2ならエージェント起動やcleanupを中断します。
Anthropicのworktree公式文書も、worktreeを「独自のファイルとブランチを持ち、main checkoutとリポジトリ履歴・remoteを共有する別working directory」と説明しています。つまり、ファイル編集の分離と実行環境の分離を混同してはいけません。
対象worktreeだけで分離追加対策trackedファイルの作業コピーはいエージェントごとに別branchを割り当てるGit履歴・refs・remoteいいえ統合順序とpush権限を決めるgitignored・untrackedファイル自動コピーされない安全な初期化手順を用意するポート・DB・キャッシュいいえ名前、番号、保存先をエージェント別にするOS権限・秘密値・networkいいえsandbox、最小権限、秘密管理を併用するマージ競合いいえ担当ファイルを分け、統合テストを行う
Git公式のgitignore文書によると、gitignoreの目的は「Gitが追跡していないファイルを未追跡のままにする」ことです。したがって、main checkoutの.envをignoreしても、新worktreeへ複製される根拠にはなりません。Claude Codeにはgitignoredファイルを新worktreeへ持ち込む.worktreeincludeがありますが、秘密値を無差別に複製せず、必要な雛形・ローカル設定だけをレビューして選びます。
本番リポジトリではなく、一時リポジトリで境界を確認します。最低1コミットを作り、エージェントごとに別branchを割り当てます。
mkdir worktree-lab && cd worktree-lab
git init -b main
git config user.name "Worktree Test"
git config user.email "test@example.invalid"
printf '.env\n' > .gitignore
printf 'TEST_ONLY=1\n' > .env
printf 'base\n' > shared.txt
printf 'base-a\n' > a.txt
printf 'base-b\n' > b.txt
git add .gitignore shared.txt a.txt b.txt
git commit -m initial
git worktree add -b agent-a ../agent-a
git worktree add -b agent-b ../agent-b
git worktree list --porcelainmain側の.envはcommit対象ではないため、../agent-a/.envと../agent-b/.envには現れません。初期化に秘密値が必要なら、値をGitへ追加するのではなく、秘密管理サービスや権限を絞ったローカル手順から各worktreeへ供給します。
次をworktree_guard.pyとして保存します。git worktree list --porcelainを機械可読形式として解析し、存在しないworktree、重複branch、status取得失敗、tracked・untracked変更を検出します。clean時は何も出力しません。
#!/usr/bin/env python3
from collections import Counter
from pathlib import Path
import subprocess, sys
def git(repo, *args):
return subprocess.run(
["git", "-C", str(repo), *args],
text=True, capture_output=True, check=False)
def records(text):
result = []
for block in text.strip().split("\n\n") if text.strip() else []:
item = {}
for line in block.splitlines():
key, _, value = line.partition(" ")
item[key] = value
result.append(item)
return result
def inspect(repo):
listed = git(repo, "worktree", "list", "--porcelain")
if listed.returncode:
return ["worktree-list-failed"]
items = records(listed.stdout)
counts = Counter(x["branch"] for x in items if "branch" in x)
problems = [f"duplicate-branch: {b}" for b, n in counts.items() if n > 1]
for item in items:
raw = item.get("worktree", "")
path = Path(raw)
if "prunable" in item or not path.is_dir():
problems.append(f"missing: {raw}")
continue
status = git(path, "status", "--porcelain", "--untracked-files=normal")
if status.returncode:
problems.append(f"status-failed: {raw}")
elif status.stdout:
problems.append(f"dirty: {raw}")
return sorted(set(problems))
def main():
if len(sys.argv) != 2:
print("usage: worktree_guard.py REPOSITORY", file=sys.stderr)
return 64
problems = inspect(Path(sys.argv[1]).resolve())
if problems:
print("\n".join(problems), file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())このguardは削除を自動実行しません。検出と破壊操作を分けることで、誤判定が直ちに成果消失へつながらない設計です。dirty時に自動で--forceを付ける処理は追加しないでください。
Pythonのtempfile.TemporaryDirectory内で毎回リポジトリを作れば、手元のコードを壊さず成功系と失敗系を実行できます。テストすべき契約は次の5件です。
agent-aのtracked変更がagent-bへ即時反映されず、両方に.envがない。
全worktreeがcleanならguardはexit 0・完全無出力、untracked成果追加後はexit 2・stdout 0 byteになる。
agent-aがa.txt、agent-bがb.txtをcommitした場合、mainへ順番にマージできる。
両agentがshared.txtの同じ行を変更すると、2本目のmergeがnonzeroになり、unmerged pathを検出できる。
untracked成果があるworktreeへの通常のgit worktree removeは失敗し、ディレクトリを残す。
以下はテストの中心部分です。リポジトリ作成処理をsetUpへ置き、各テストの前に上の作成コマンド相当を実行します。
def test_separation(self):
(self.a / "a.txt").write_text("agent-a\n")
self.assertEqual((self.b / "a.txt").read_text(), "base-a\n")
self.assertFalse((self.a / ".env").exists())
self.assertFalse((self.b / ".env").exists())
def test_guard_contract(self):
clean = run(PYTHON, GUARD, self.repo)
self.assertEqual((clean.returncode, clean.stdout, clean.stderr), (0, "", ""))
(self.a / "result.txt").write_text("uncommitted\n")
dirty = run(PYTHON, GUARD, self.repo)
self.assertEqual((dirty.returncode, dirty.stdout), (2, ""))
self.assertIn("dirty:", dirty.stderr)
def test_non_conflicting_merge(self):
commit(self.a, "a.txt", "from-a\n")
commit(self.b, "b.txt", "from-b\n")
self.assertEqual(git(self.repo, "merge", "--no-edit", "agent-a").returncode, 0)
self.assertEqual(git(self.repo, "merge", "--no-edit", "agent-b").returncode, 0)
def test_conflicting_merge(self):
commit(self.a, "shared.txt", "from-a\n")
commit(self.b, "shared.txt", "from-b\n")
self.assertEqual(git(self.repo, "merge", "--no-edit", "agent-a").returncode, 0)
self.assertNotEqual(git(self.repo, "merge", "--no-edit", "agent-b").returncode, 0)
def test_dirty_remove_is_refused(self):
(self.a / "result.txt").write_text("uncommitted\n")
removed = git(self.repo, "worktree", "remove", str(self.a))
self.assertNotEqual(removed.returncode, 0)
self.assertTrue(self.a.is_dir())実際に標準ライブラリだけの完全版を実行した結果はRan 5 tests in 1.183s、OKでした。直接contract確認では、clean時がexit=0 / stdout=0 byte / stderr=0 byte、untracked成果追加後がexit=2 / stdout=0 byte / stderr=98 byteでした。診断にはOS一時ディレクトリの絶対パスが入るため、stderrのbyte数を固定値としてテストせず、終了コード、stdoutの沈黙、dirty:の存在を契約にします。
guardのstdoutやstderrを検査対象worktree内の新規ファイルへredirectすると、そのファイルをguardがuntracked成果として検出します。復旧はログをOSの一時領域またはリポジトリ外へ保存することです。プロジェクト内に残す必要がある生成物は、成果物としてcommitするか、チーム合意のignore方針を明示します。
worktreeは編集時の上書きを避けますが、同じ起点から同じ行を変更した事実は消しません。競合後はgit diff --name-only --diff-filter=Uで対象を確認し、内容をレビューして解消し、全テストを再実行します。lockfile、DB schema、共通型、共通設定を触るタスクは直列化するか、統合担当を1つに固定する方が安全です。
通常のremoveが拒否したら、まずgit -C PATH status --short、git -C PATH log --oneline --branches --not --remotesで未コミット・未push成果を確認します。必要な変更をcommitしてremoteへ退避するか、明示的に破棄すると判断した後だけcleanupします。手動のrm -rfや反射的な--forceは復旧手順ではありません。
別worktreeでも、両プロセスがlocalhost:3000や同じDB schemaへ接続すれば干渉します。PORT、DB名、cache directory、コンテナproject名、テストユーザーをworktree名から決定的に派生させます。秘密値はbranchへcommitせず、必要最小限だけ注入します。
AI並列開発でのgit worktree活用例でも、worktreeはGitデータの実体を共有し、別ディレクトリで別branchを扱う方法として紹介されています。基本操作を覚えた後は、共有境界とcleanup失敗をテストへ残すことが実運用の差になります。
方法向くタスク残る注意git worktree同一repoで担当ファイルが離れた短期並列作業履歴・remote・サービス資源は共有git cloneGit管理領域も別管理したい検証同期はfetch・pushが必要container・VM依存物、port、process、filesystem境界が重要秘密管理とremote競合は別設計直列実行同じ行、schema、lockfile、統合点を触る作業並列性よりレビュー容易性を優先
Claude Codeの--worktreeやsubagentのisolation: worktreeは作成を省力化できますが、境界そのものはGit worktreeです。CodexやCursorで手動作成したworktreeを開く場合も、同じ5テストとguardを適用できます。製品固有機能を使ったから安全になるのではなく、担当範囲、環境初期化、統合順序、cleanup条件を明示したときに安全性が上がります。
なおGit公式文書は、superprojectの複数checkoutにおけるsubmodule対応が不完全であると明記しています。submoduleを含むrepoではこの記事の結果をそのまま一般化せず、対象構成で追加検証してください。
タスクごとに別branch・別worktreeを作り、同じ行や統合点を担当させない。
各worktreeで依存物、必要なローカル設定、秘密値の供給方法を初期化する。
port、DB、cache、外部sandbox、テストアカウントを分ける。
worktree_guard.pyのclean時完全無出力とdirty時exit 2をCIまたは起動wrapperで検査する。
各branchでテストを通し、統合担当が1本ずつmainへmergeして再テストする。
cleanup前にdirty・untracked・未push commitを確認し、強制削除を自動化しない。
最初の一歩は、上のguardを一時リポジトリへコピーし、5件のテストを自分のGit環境で実行することです。AIエージェントの並列数を増やす前に、「編集は分離できたが、環境と統合は共有される」という境界を失敗テストで固定してください。
Claude CodeのPreToolUse HookをPython標準ライブラリだけで実装し、許可時の沈黙、拒否JSON、不正入力、パストラバーサル、symlink脱出を8ケースの自動テストと実CLIで検証します。
Claude Codeの/loopは、指定した時間ごとにAIへ同じお願いを自動で繰り返す機能です。デプロイ完了待ちやPR確認の「見張り」を任せられます。前提知識から使い方、注意点まで、初心者向けに整理します。
OpenAI CodexやGoogleの開発者向けAI発表をもとに、AIコーディングエージェントがシステム開発の現場に与える変化を、企業の実務目線で整理します。