Claude Code のセッションを worktree ごとに分けて並列で動かしていても、同じディレクトリで2つのセッションが動くことがあります。このとき一方が git checkout や git add -A を実行すると、もう一方の編集が消えたり、コミットに混ざったりします。この記事では、PreToolUse hook でこれを止める checkout-guard が、「別の生きたセッションが同じ git ツリーで作業している」ことをどう判定しているかを、実装に沿って説明します。自作の OSS(claude-bulkhead、MIT)の話ですが、hook から他セッションを見つける方法として単体で使える内容にしています。
入力と出力
PreToolUse hook は、実行しようとしているツール呼び出しを JSON で標準入力から受け取ります。終了コード 0 なら実行され、終了コード 2 で標準エラー出力にメッセージを書けば、呼び出しは取り消されてそのメッセージが Claude に渡ります。checkout-guard の入力は Bash のコマンド文字列で、出力は次のような文面です。
[bulkhead] checkout-guard: `git add -A` stages every change in /home/me/src/acme-web (7 files), and another Claude session works in the same tree (pid 18672, started 2026-10-13 10:40).
Some of those changes are probably its half-finished work, and they would go into your commit.
Stage only the files you changed, by name: git add src/a.ts src/b.ts, or commit them directly with git commit -m "..." -- src/a.ts src/b.ts.
Claude は理由と代わりのコマンドを受け取るので、ユーザーに確認せず、ファイル名を指定して再実行できます。
他のセッションを見つける
Claude Code は、起動中のセッションごとに ~/.claude/sessions/<pid>.json を書きます。ここには PID、セッション ID、プロジェクトのディレクトリ(cwd)、開始時刻が入っています。checkout-guard はこのファイルを全部読み、3つの条件をすべて満たすものを「他のセッション」と見なします。まず、その PID のプロセスが生きていることです。終了したセッションのファイルが残っていても数えません。次に、セッション ID が自分のものと違うことです。サブエージェントは親と同じセッション ID を使うので、親子同士で止め合うことはありません。最後に、cwd が自分と同じ git rev-parse --show-toplevel の内側にあることです。
この最後の条件が要点です。cwd が完全に一致するかではなく、git のツリーのルートで比べます。そのため、同じリポジトリの web/ の下で動いているセッションも、同じツリーとして扱われます。逆に、入れ子になった別のリポジトリで動いているセッションは、トップレベルが違うので対象になりません。Windows では大文字小文字を区別せずに比べます。
いつ git を呼ぶか
毎回 git を呼ぶと遅くなるので、順番を決めています。まずコマンド文字列がチェックアウトやステージングに当たるかを見ます。当たらなければ、そこで通します。当たったときだけ、他に生きているセッションがいるかを調べ、いなければ通します。いる場合に初めて git を呼んでツリーのルートと変更の有無を確認します。git add -A は、ツリーに変更があるときだけ止めます。1回の判定は通常 50 ミリ秒ほどで、大半は Node の起動時間です。
止める対象は、ブランチやファイルを動かすコマンド(git checkout main、git switch -c、git stash、git reset --hard、git clean -f、git rebase、git merge、git restore .)と、全変更をまとめて取るコマンド(git add -A、git add .、git commit -a)です。git add src/a.ts のようにファイル名を指定すれば通ります。bash -c "git checkout main" のようなラッパーや GIT_TRACE=1 のような前置きは、中身を開いて判定します。
テストでの確認
テストでは git の応答とセッション一覧を表で与えています。セッション100(自分)、200(同じツリーの web/ 配下)、300(別のリポジトリ)、400(入れ子のリポジトリ)、500(終了済み)を用意し、200 がいるときだけ止まること、300 と 400 と 500 しかいなければ通ることを確認しています。ブロックのメッセージに pid 200, started 2026-10-13 10:40 が含まれることも検証しています。
限界
起動時のディレクトリが別で、あとから cd でこのツリーに入ってきたセッションは、セッションファイルの cwd が違うので見つかりません。また、ssh 経由で送られる git コマンドは判定の対象外で、コマンド文字列を読む仕組みなのでサンドボックスの代わりにもなりません。
自分の履歴で試す
過去のセッション記録(~/.claude/projects)を hook に再生する bulkhead scan も付いています。記録には当時のセッション一覧がないため、checkout-guard の分は、コマンドの前後10分以内に同じ git ツリーで動いていたセッションから組み立て直します。自分の環境の2週間分(7.0GB、シェルコマンド約10万件)は 39 秒で読め、同時に動いたセッションは最大 13 本、止められていたはずのコマンドは 507 件、うち 25 件が他セッションの編集中のツリーでの git add -A でした。読み取りだけで、外部には送信しません。
/plugin marketplace add dooly6276/claude-bulkhead
/plugin install bulkhead@bulkhead
/bulkhead:scan 30d
リポジトリ: https://github.com/dooly6276/claude-bulkhead (docs/guards/checkout-guard.md に詳細があります)