並列 Claude Code の git checkout で作業中のコードが書き換わるのでgit worktree + hook で解決した

Robots causing chaos around a yellow folder labeled 'git checkout', with warning icons, arrow pointing to organized worktree folders labeled 'git worktree' on the right

PSSLの佐々木です

最近は Claude Code を複数セッション同時に立ち上げて、機能Aの実装・機能Bのレビュー・ドキュメント整備…と並列で作業させるのが当たり前になってきました。便利なのですが、セッションで git checkout が走った瞬間、別セッションが作業中のコードが丸ごと別ブランチの内容に変わってしまう問題が起きたので解決しました。

この記事では、

  • なぜ複数の AI エージェントと git checkout の相性が悪いのか
  • git checkout と git worktree の仕組みの違い
  • worktree を使うと何がうれしいのか
  • ルールで縛るだけでは守られないので hook で checkout を強制ブロックする 方法
  • worktree 運用のハマりどころ

についてまとめました。

1. 何が起きたのか

Claude Code のセッションは、基本的にリポジトリの作業ディレクトリ(チェックアウト)を共有して動きます。ここで複数セッションを並列で走らせると、こういうことが起きます。

セッションA: feature/foo を実装中(ファイル編集中…)
セッションB: 「PR #95 をレビューして」→ git checkout feature/bar 実行
                    │
                    ▼
セッションAが見ているファイルが全部 feature/bar の内容に変わる

git checkout(や git switch)は「今いるディレクトリの中身をそのブランチの状態に書き換える」コマンドです。つまり作業ディレクトリというグローバルな状態を破壊的に切り替えます。1人の人間が1つのターミナルで作業していた時代はそれで良かったのですが、複数のエージェントが同じディレクトリで同時に動く前提だと、

  • セッションAの編集途中ファイルとセッションBの checkout が衝突する
  • テストが「なぜか」落ちる(実は別ブランチのコードを実行している)
  • コミットが意図しないブランチに載る
  • 最悪、未コミットの変更が checkout に巻き込まれて消える

という、デバッグしても原因にたどり着きにくい系の事故になります。AI エージェントは checkout を「ためらわない」ので、人間同士よりも踏む確率が高いです。

2. git checkout と git worktree の違い

git worktree は「1つのリポジトリから、複数の作業ディレクトリを生やす」機能です。Git 2.5(2015年)からある機能ですが、AI エージェント並列時代になって急に価値が上がったと感じています。

git checkout 方式(状態の切り替え)

  repo/  ←─ ここが feature/foo になったり feature/bar になったりする
             (同時には1つのブランチしか存在できない)

git worktree 方式(ディレクトリの追加)

  repo/                        ← develop のまま
  worktrees/feature-foo/       ← feature/foo 専用ディレクトリ
  worktrees/feature-bar/       ← feature/bar 専用ディレクトリ
             (.git の実体は repo/ と共有。HEAD だけ worktree ごとに独立)

仕組みとしては、.git のオブジェクト(コミット・blob)は全 worktree で共有され、HEAD やインデックスだけが worktree ごとに分かれます。なので clone と違ってディスクをほぼ食わず、fetch も1回で全 worktree に反映されます。

git checkout / switchgit worktree
ブランチの持ち方1ディレクトリに1つ(切り替え)ブランチごとに別ディレクトリ(並存)
他の作業への影響ある(作業ディレクトリ全体が変わる)ない(各ディレクトリが独立)
.git の実体そのまま共有(ディスク効率が良い)
並列作業不可能可能
fetch / stash / オブジェクト全 worktree で共有

基本操作はこれだけです:

# ブランチ用の worktree を作る(ブランチが無ければ -b で作成)
git worktree add ../worktrees/feature-foo feature/foo
git worktree add -b feature/new ../worktrees/feature-new

# 一覧
git worktree list

# 終わったら削除
git worktree remove ../worktrees/feature-foo
git worktree prune   # 消し忘れの掃除

3. なぜ AI エージェント並列時代に worktree が注目されているのか

  • セッションごとに独立した世界を渡せる — セッションAは worktrees/feature-foo/、セッションBは worktrees/feature-bar/ で作業。お互い何をしても干渉しません。
  • メインのチェックアウトが常に安定する — repo/ は develop のまま置いておけるので、「今の正の状態」を見失わない。人間が確認する場所としても安心です。
  • Claude Code がネイティブ対応している — Claude Code には worktree サポートが組み込まれていて、claude --worktree や EnterWorktree、サブエージェント起動時の isolation: "worktree" を使うと、一時的な worktree を自動で作って作業し、変更が無ければ自動で掃除までしてくれます。つまり「並列にエージェントを走らせるための公式な答え」がすでに worktree なんですね。
  • レビューと実装を同時にできる — 「PR のブランチを checkout してレビュー」の代わりに「PR のブランチを worktree に生やしてレビュー」にすれば、実装中のセッションを止めずに済みます。

4. ルールで縛るだけでは守られないので hook で強制する

「ブランチ切り替えは worktree を使ってください」と書いても、コンテキストが長くなったセッションや、レビュー依頼を受けたエージェントが「じゃあ checkout しますね」とやってしまう可能性は消せません。

そこで Claude Code の PreToolUse hook で、共有ディレクトリでの git checkout / git switch を仕組みとしてブロックすることにしました。hook は Claude が Bash コマンドを実行する直前に必ず割り込めるので、ルールと違って「忘れる」ことがありません。

4.1 ガードスクリプト

scripts/worktree-guard-hook.sh を作ります:

#!/bin/bash
# Worktree guard hook for Claude Code (PreToolUse / Bash)
# 共有作業ディレクトリでの git checkout / git switch によるブランチ切り替えをブロックする

set -euo pipefail

hook_input=$(cat)
command=$(echo "$hook_input" | jq -r '.tool_input.command // empty')

[ -z "$command" ] && exit 0

# git のサブコマンドとして checkout / switch が呼ばれているか
# (git の直後、またはグローバルオプション -C/-c/--xxx を挟んだ直後のみマッチ)
git_switch_re='(^|[;&|(]|&&|\|\|)[[:space:]]*git([[:space:]]+(-C[[:space:]]+[^[:space:]]+|-c[[:space:]]+[^[:space:]]+|--[A-Za-z0-9-]+(=[^[:space:]]+)?))*[[:space:]]+(checkout|switch)([[:space:]]|$)'

if echo "$command" | grep -qE "$git_switch_re"; then
    # ファイル復元形式 (`git checkout [<ref>] -- <path>`) は許可
    if echo "$command" | grep -qE 'checkout[^;&|]* -- '; then
        exit 0
    fi
    jq -n '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "🚫 共有作業ディレクトリでのブランチ切り替え(git checkout/switch)は禁止です。他のセッションと同じチェックアウトを共有しています。代わりに git worktree を使ってください: `git worktree add ../worktrees/<branch> <branch>` して、そのディレクトリ内で作業する。ファイル復元だけなら `git checkout -- <file>` / `git restore <file>` は許可されています。"
      }
    }'
    exit 0
fi

exit 0

ポイントは単純な grep checkout にしていないことです。それだと git commit -m "switch to new API" のような無害なコマンドまで誤爆するので、「git のサブコマンド位置に checkout/switch が来た場合」だけをブロックし、さらにブランチが変わらないファイル復元形式git checkout -- <file>)は素通しにしています。

4.2 settings.json に登録

.claude/settings.json(プロジェクト設定 = git にコミットするのでチーム全員に効く)に登録します:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/worktree-guard-hook.sh",
            "if": "Bash(git *)",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

$CLAUDE_PROJECT_DIR を使っておくと、worktree の中でセッションを起動したときもパスが解決できます。if フィルタで git コマンドのときだけ hook を起動するようにして、無駄なプロセス起動も避けています。

4.3 動作確認

Claude に checkout させようとすると、実行前にこう弾かれます:

> git checkout develop

🚫 共有作業ディレクトリでのブランチ切り替え(git checkout/switch)は禁止です。
   代わりに git worktree を使ってください: ...

deny の理由メッセージはそのまま Claude にフィードバックされるので、Claude は自分で git worktree add に切り替えて作業を続行します。「ブロックして終わり」ではなく「正しい道を教えて誘導する」のが hook メッセージ設計のコツだと思います。

あわせて CLAUDE.md にも同じルールを書いておきます。hook が「強制」、CLAUDE.md が「事前ガイダンス」の二段構えで、そもそも deny を踏む前に worktree を選んでくれるようになります。

### Git
- **ブランチ切り替え禁止(worktree 必須)**: 共有作業ディレクトリでの
  `git checkout` / `git switch` は禁止(hook でブロックされる)。
  別ブランチで作業するときは `git worktree add ../worktrees/<branch> <branch>` を使う。

5. 注意点(ハマりどころ)

worktree に寄せるにあたって、いくつか知っておいたほうがいいことがあります。

5.1 同じブランチは2つの worktree でチェックアウトできない

Git の仕様で、あるブランチをチェックアウトできる worktree は同時に1つだけです。「メインで develop を開いたまま、worktree でも develop を」はできません(インデックスが壊れるのを防ぐための制約なので妥当です)。レビューや検証で同じコミットを見たいだけなら git worktree add --detach で detached HEAD にすると回避できます。

5.2 node_modules や .venv は付いてこない

worktree は git 管理下のファイルしか持ってきません。node_modules/ や Python の .venv/ は worktree ごとに作り直しが必要です。毎回 npm ci するとディスクも時間も食うので、Claude Code の設定でシンボリックリンクを張るのが楽です:

{
  "worktree": {
    "symlinkDirectories": ["node_modules", "frontend/node_modules"]
  }
}

ただし symlink 共有は「両方の worktree で依存バージョンが同じ」前提なので、lockfile をいじるブランチでは素直に入れ直したほうが安全です。

5.3 .env などの git 管理外ファイルもコピーされない

secret 類を .env に置いている場合、worktree には存在しないのでアプリが起動しません。前回記事の Infisical のように「secret をファイルとして持たない」構成にしておくと、worktree 運用とも相性が良いです(infisical run はどのディレクトリからでも同じように動く)。

5.4 stash・fetch・オブジェクトは共有される

.git の実体は共有なので、git stash の中身や fetch 済みの ref は全 worktree から見えます。「worktree ごとに完全に独立した Git 状態」ではない点は頭に入れておくと混乱しません。逆に fetch が1回で済むのはメリットです。

5.5 worktree の消し忘れが溜まる

並列作業が捗るほど worktree が増えます。ディレクトリを rm -rf で消しただけだと Git 側に管理情報が残るので、

git worktree remove <path>   # 正しい消し方
git worktree prune           # rm してしまった残骸の掃除
git worktree list            # 定期的に棚卸し

を習慣にするのがおすすめです。Claude Code の自動 worktree(EnterWorktree / isolation)は変更が無ければ自動で掃除してくれるので、手動運用よりこちらに寄せるとゴミが出にくいです。

5.6 起動中のサーバーのパスに注意

dev server やテストランナーは「起動したディレクトリのコード」を見続けます。メインのチェックアウトで npm run dev を立ち上げたまま worktree 側を編集しても、当然サーバーには反映されません。動作確認はその worktree の中でプロセスを立ち上げる、を徹底する必要があります。

6. まとめ

  • git checkout は作業ディレクトリというグローバル状態の破壊的な切り替え。複数の Claude Code セッションを並列で走らせる環境では、他セッションの作業を巻き込む事故のもと
  • git worktree ならブランチごとに独立したディレクトリを生やせるので、セッション同士が干渉しない。.git は共有なのでディスクも fetch も効率的
  • Claude Code は worktree をネイティブサポートしている(-worktree / EnterWorktree / isolation: "worktree")ので、並列エージェント運用の公式な答えはすでに worktree
  • ただし CLAUDE.md に書くだけは「お願い」レベル。PreToolUse hook で git checkout / git switch を構造的にブロックし、deny メッセージで worktree へ誘導するのが確実
  • 注意点は「同一ブランチの重複チェックアウト不可」「node_modules / .env は付いてこない」「worktree の掃除」あたり

「ルールを書いたから大丈夫」ではなく「仕組みで事故れないようにする」。secret 管理のときと同じ結論に落ち着きましたが、AI エージェントと並走する開発では、この考え方があらゆる場面で効いてくると感じています。

参考リンク

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です