git stash -u && git checkout -B feature/x origin/dev && git stash pop ——手が覚えているレベルで何百回と打ってきたはずのワンライナーだった。ところがある日だけ様子がおかしい。stash -uは「保存するローカル変更がありません」と素っ気なく返してきた(そう、いつも通りのはずだった)。なのに直後のstash popが盛大にコンフリクトを起こす。ローカル変更なんてなかったはずなのに、いったい何をpopしたというのか。
正体は、そのworktreeに前のセッションから残っていた無関係な古いstashだった。Claude CodeのようなAIコーディングエージェントを、承認なしで非対話実行されるsandbox環境やスケジュールタスクで動かし、同じgit worktreeを使い回していると、同じ罠を踏む可能性がある。自分の環境のworktreeに、片付け忘れの古いstashが本当に一つも残っていないと言い切れるだろうか。しかも復旧しようとしたgit reset --hardは、sandboxの安全機構に「破壊的操作」としてブロックされ、非対話実行中で確認も取れない(誰かに「どうしましょう」と聞きたくても、聞く相手が画面の外にいる)。この記事では、この詰みかけの状況を非破壊コマンドの組み合わせだけで抜けた手順を、読者が自分の環境で再現できる形で整理する。
この記事で学べること
- 長寿命worktreeで
stash popが「自分が今stashした分だけ戻る」という思い込みで事故る理由 git statusのUU/DU表示の見分け方と、コンフリクトごとの非破壊な解消コマンドgit reset --hardが使えない状況で、同じ着地点に非破壊コマンドの組み合わせで到達するパターン
前提条件
- git worktreeで複数のブランチ作業ディレクトリを使い回している
- Claude Code(Pro $20/月、Max $100〜200/月)のような、承認なしで非対話実行されるAIコーディングエージェント環境、あるいはCIなど、
git reset --hardやgit clean -fが権限設定でブロックされている環境 git stash/git checkoutの基本操作
何が起きたか
git stash popは「自分が直前にstashしたエントリ」を戻すコマンドだと思われがちだが、実際に対象にしているのは**stashリストの先頭stash@{0}**でしかない。同じworktreeを日をまたいで使い回していると、以前の作業やプロセスがgit stash pushだけして片付け忘れたエントリが、リストの先頭に居座ったままになる。
その状態でgit stash -uを打つと「今のworking directoryには保存すべき変更がない」のでNo local changes to saveと表示される。ここで「何も積まれていない」と誤解しやすいが(会議室が「空いてます」に見えて、実は前の予約が終わっていなかった、あの現象に近い)、popの対象であるstash@{0}はスタック済みの古いエントリとして既に存在している。そのままgit checkout -B feature/x origin/devでブランチを最新化し、git stash popを叩くと、自分が意図していない古い差分が作業中のファイルに適用され、複数ファイルでコンフリクトになる。
さらに詰む: git reset --hard も使えない
コンフリクトを見た瞬間、いちばん早い逃げ道は「全部リセットしてやり直す」ことだ。ところがsandbox環境では、git reset --hardやgit clean -fのような、未コミットの変更を問答無用で消すコマンドが権限設定でブロックされていることがある。Claude Codeのpermissions.denyはその一例で、"Bash(git reset --hard:*)"のようなパターンを明示的に拒否リストへ入れておく運用が知られている。狙いは単純で、AIエージェントが「きれいにしておきますね」と善意で打ったコマンドが、未コミットの変更ごと消し飛ばす事故を防ぐことだ。
普段この制約は「安全のための多少の不便」でしかないが、非対話実行中に自分がその制約に引っかかると話は変わる。確認を求めるプロンプトは出ない。ブロックされたコマンドは単に失敗する(お伺いのポップアップすら出ない。ただ黙って弾かれるだけだ)。ここで無理にブロックを迂回しようとするのではなく、同じ着地点に非破壊コマンドの組み合わせで到達する方向に頭を切り替える必要がある。
非破壊で復旧する手順
まずgit statusとgit ls-files -sで、コンフリクトの中身をステージ単位で確認する。
| 表示 | 意味 | 対処 |
|---|---|---|
UU(both modified) | 自分側の変更とstash側の変更が同じファイルを触った | git checkout --ours -- <path> で自分側の内容を採用してからadd |
| (無関係な差分) | stash由来の変更で、今の作業と関係ない | git checkout HEAD -- <path> で丸ごと破棄 |
DU(deleted by us) | 自分側では既に削除済みのファイルに、stash側が変更を加えていた | ワークツリー上のファイルを物理的に削除してからaddし、削除を確定させる |
UUは素直に「どちらの内容を残すか」で解決できるが、厄介なのはDUだ。自分側(ours)では既に削除しているファイルに対して、popしてきたstashが変更を加えている状態なので、git rmだけでは「unmerged path」として弾かれることがある。ここでrm -rfのような直接削除も権限設定でブロックされている場合、rip(rmの代わりに使う、削除したファイルを一時領域へ退避させる回復可能な削除ツール)でワークツリー上のファイルを物理的に消し、そのうえでgit add <path>を打つと、「このパスは削除で解決した」という意思表示としてGitに伝わる。
すべてのコンフリクトを解消したら、最後にgit checkout -B feature/x origin/devでブランチを最新のリモート状態に張り直す。個別のファイルをちまちま直すより、コンフリクト解消が終わった時点でブランチごと綺麗に作り直したほうが、取りこぼしのリスクが小さい。
動作確認
同じ状況は、自分の環境でも次のコマンドで再現できる。
# 1. デモ用リポジトリを準備する
mkdir stash-pop-demo && cd stash-pop-demo
git init -q
echo "base" > shared.txt
git add shared.txt
git commit -qm "base commit"
git checkout -qb dev
# 2. 「前のセッションが片付け忘れたstash」を再現する
echo "leftover change from an old session" >> shared.txt
git stash push -u -m "leftover from another session"
# 3. 今回の作業では、まだ何もローカル変更がない状態から始める
git checkout -qb feature/today dev
# 4. 手癖のワンライナーを実行する
git stash -u # → "No local changes to save" と出るが…
git stash pop # → shared.txt でコンフリクトが起きる
# 5. コンフリクトの状態をステージ単位で確認する
git status
git ls-files -s shared.txt
ここまででshared.txtがUUとして表示されるはずだ。以降は解消パターンごとに次を試す。
# 6a. 自分側の内容を採用してよい場合
git checkout --ours -- shared.txt
git add shared.txt
# 6b. stash由来の差分が今の作業と無関係なら、丸ごと破棄する場合
git checkout HEAD -- shared.txt
# 7. 最後にブランチを綺麗に張り直す(リモートがなければ dev を直接指定)
git checkout -B feature/today dev
DU(削除-変更コンフリクト)を試したい場合は、手順2でshared.txtをstashする前にgit rm shared.txt && git commitしておき、手順4のpop後にgit ls-files -sでDUが出ることを確認したうえで、rip shared.txt && git add shared.txtで解消できる。
注意点・Tips
stash popは「今stashした分」ではなく「stashリストの先頭」を対象にする: 長寿命worktreeを使い回すなら、popの前にgit stash listで何が積まれているかを確認する癖をつけたほうがいい(「たぶん自分のだろう」で叩くと、今回のような事故を踏む)- コンフリクトしたpopは、stashエントリを消さない:
git stash popはマージに失敗すると、stash自体をdropしない仕様になっている。つまりコンフリクトで慌てても、元のstashはgit stash listから消えておらず、何も失われていない - sandboxの破壊的操作ブロックは、邪魔者ではなく非対話実行時の安全弁: ブロックされたら無理に突破しようとせず、
checkout --ours/checkout HEAD --/ 回復可能な削除ツール /checkout -Bのような非破壊コマンドの組み合わせで、同じ結果に到達できないかをまず考える
まとめ
長寿命のworktreeを使い回していると、stash popは「新しく積んだ分だけ戻す」という思い込みで事故る。stash -uが「保存する変更がない」と言っても、pop対象のstash@{0}は既に別物として存在している可能性がある。そこにsandbox環境の破壊的操作ブロックが重なると、いつものgit reset --hard一発では逃げられない(頼みの綱のはずが、まさかの通行止め)。
あなたの環境でAIエージェントやCIが非対話でgit操作をしているなら、一度git statusのUU/DU表示と、checkout --ours / checkout HEAD -- / 回復可能な削除ツールでの物理削除 / checkout -Bでの張り直し、という非破壊の組み合わせを手元で試しておいてほしい。破壊的操作がブロックされる場面に実際に出くわしたときになって初めて調べるより、先に手順を知っておくほうがずっと早く抜けられる。



