git worktreeは、本流(main)を汚さずに別ブランチの作業を物理的に分けるための仕組みだ。並行して機能開発を進める人や、AIエージェントに作業を任せる人ほど「worktreeに隔離しておけば本流は安全」と考える。claude -w <名前> でworktreeのセッションを開けば、編集はそのworktreeの中だけに閉じる——はずだった。
ところが2026年6月22日に上がった起票 #70069 では、worktreeで作業していたつもりが、約70件の編集がすべて本流(master)側のファイルに着地していたという報告がある。worktreeのブランチはほぼ空のまま、実際の成果は全部mainに入っていたそうだ。しかも作業中、警告は一度も出なかったという。
筆者はこの事故を自分では踏んでいないので、起票の内容は伝聞として紹介する。ただ、利用者の側で先回りして止める方法は実際に手元で組んで動かして確かめたので、そこは断定で書く。
何が起きたか(起票#70069の報告)
報告によると、セッションの記録(transcript)は最後まで正しかったらしい。
- 作業ディレクトリ(
cwd)は<リポジトリ>/.claude/worktrees/<名前>を指していた - ブランチ(
gitBranch)もworktree-<名前>を指していた
つまりハーネスは「自分はworktreeの中にいる」と正しく認識していた。にもかかわらず、約70回の Edit/Write のうち1件を除く全部が、worktreeではなく本流のチェックアウト(<リポジトリ>/packages/...)の絶対パスを書き換えていた、とある。
なぜそうなるのか
起票が挙げている原因はこうだ。モデルは、メモ(auto-memory)などに書かれた「リポジトリのルートからの相対パス」(例: packages/rrweb/src/record/foo.ts)を絶対パスに変換するとき、worktreeのルートではなくリポジトリのルートに紐づけた。
ここが厄介なところで、worktreeはリポジトリの内側の .claude/worktrees/<名前> に存在する。だから <リポジトリ>/packages/... という絶対パスは、「存在する正しいファイル」なのだ。ただしコピー違い——本流側のファイルである。読み込みも編集も普通に成功してしまうので、エラーも警告も出ない。worktreeの中の同じ相対位置のファイルには、一度も触れないまま終わる。
結果として、隔離しているつもりの作業が本流に直接書き込まれ、そのままコミットされる。git履歴で追えるとはいえ、その本流のブランチがあとで rebase や force-push で上書きされていれば、作業は丸ごと失われる。「隔離していたつもり」という確信があるぶん、気づくのが遅れる。データ消失と同じ型の事故だ。
利用者の側でどう止めるか
この事故の良いところ(と言ってよければ)は、判定が決定論的に書けることだ。確率的な分類器に頼らなくても、文字列の比較だけで「編集がworktreeを脱出して本流を指していないか」を確実に判定できる。
考え方はこうだ。
- セッションの
cwdが.claude/worktrees/<名前>を含むなら、worktreeのセッションだと判定する -
cwdから、リポジトリのルートとworktreeのルートを取り出す - 編集対象の絶対パスが、リポジトリのルートの下にあるのに、worktreeのルートの下にはないなら、それは本流側のコピーを指している→止める
相対パスは cwd(=worktreeの中)を基準に解決されるので安全、リポジトリの外を指す絶対パスはこのガードの担当外(別のガードが見る)、という切り分けにすると誤検知が出にくい。
PreToolUse のフックとして、Edit/Write/MultiEdit を対象に実装した例が次だ。これは実際に手元で動かし、16通りの入力(脱出する編集はブロック、worktree内・相対パス・worktree外のセッション・リポジトリ外のパス・対象外のツールはすべて素通り)で期待どおりに動くことを確かめている。
#!/bin/bash
# worktree-escape-write-guard.sh
# worktreeセッション中に、編集が本流のチェックアウトへ脱出するのを止める
# 起票 #70069
set -euo pipefail
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
case "$TOOL" in Edit|Write|MultiEdit) ;; *) exit 0 ;; esac
CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
[ -z "$CWD" ] && exit 0
[ -z "$FILE" ] && exit 0
MARKER="${CC_WORKTREE_DIR_MARKER:-.claude/worktrees}"
# worktreeのセッションのときだけ働く
case "$CWD" in *"/$MARKER/"*) ;; *) exit 0 ;; esac
REPO_ROOT="${CWD%%/$MARKER/*}"
REST="${CWD#*/$MARKER/}"
WT_NAME="${REST%%/*}"
WT_ROOT="$REPO_ROOT/$MARKER/$WT_NAME"
# 相対パスはworktree基準なので安全
case "$FILE" in /*) ;; *) exit 0 ;; esac
# worktreeの中の絶対パスは許可
case "$FILE" in "$WT_ROOT"|"$WT_ROOT"/*) exit 0 ;; esac
# リポジトリの下だがworktreeの外 = 本流のコピー = 止める
case "$FILE" in
"$REPO_ROOT"/*)
echo "BLOCKED: 編集がworktreeを脱出して本流のチェックアウトを指しています。" >&2
echo " worktree: $WT_ROOT" >&2
echo " 対象: $FILE" >&2
echo "このまま書くと本流のブランチに成果が入り、worktreeは空のまま残ります(#70069)。" >&2
echo "パスを $WT_ROOT/<リポジトリ相対のパス> に直してください。" >&2
exit 2 ;;
esac
exit 0
exit 2 を返すと、Claude Codeはそのツールの実行を止めて、標準エラーに出したメッセージをモデルに戻す。だからモデルは「パスがworktreeを脱出した」と理由つきで分かり、worktreeのルートの下に書き直せる。cwd も tool_input.file_path も PreToolUse のフックの入力にそのまま入っているので、外部の状態に依存せず判定できる。
既存のガードとの違い
「作業ディレクトリがプロジェクトの外に出たら警告する」型のガードは前からある。だがこの事故はそれでは捕まらない。cwd は最後まで正しくworktreeの中に留まっているからだ。脱出しているのは cwd ではなく、個々の編集の file_path のほうだ。だから見る場所が違う。PreToolUse で file_path を見るのが正しい。
まとめ
- worktreeは隔離の仕組みだが、相対パスの解決がリポジトリのルートに紐づくと、編集が本流のコピーに静かに着地し得る(起票#70069の報告)
-
cwdは正しくても起きるので、cwdの監視では止まらない。PreToolUseでfile_pathがworktreeを脱出していないかを見る - 判定は文字列の比較だけで決定論的に書ける。誤検知も抑えられる
worktreeを使う並行開発やAIエージェントの運用では、隔離しているという確信があるぶん、事故に気づくのが遅れる。手元で打てる一行のガードで先回りしておくと、本流の汚染と、その後の rebase/force-push での消失を未然に防げる。
この種の「設定で先回りして防げるデータ消失・破壊の事故」を一冊にまとめています。/rewind でのコード消失、本番DBの削除、git stash/worktree/submodule deinit の落とし穴、Windows固有の消失、クラウド資源の削除まで、いずれも実機で検証した防ぎ方を全43章で扱っています。
- Zenn版: Anthropic公式ガイドにない事故防止——Claude Code 800+時間で19点→85点にした全記録(¥800)(第3章まで無料)
- Kindle版(¥800・Kindle Unlimited対応): Amazonで読む
- 無料のフック集(約900件・MIT): cc-safe-setup —
npx cc-safe-setup --shieldで一括導入できます