Claude Codeのresumeがworktreeで止まる原因と対処(-p・SDK)
claude -p --resumeやAgent SDKの再開が「cannot resume into worktree」で止まる理由を、3種のメッセージの読み分けから、2回目に通る条件、--fork-sessionでの回避までを扱います。
-pとAgent SDKの再開だけがエラーで止まる
worktreeの中で終えたセッションをclaude -p --resumeやAgent SDKで再開すると、Error: cannot resume into worktree <path>: ...This session was not started.のようなエラーで止まることがあります。対話モードなら同じ状況でも、隔離なしで続行するだけです。
違いは再開の設計にあります。worktreeが削除済みの場合を除いて、-pとAgent SDKの再開は拒否をそのままエラーにします。
止まる条件は次のとおりです。
- worktreeの中で終わり、ExitWorktreeで抜けていないセッションを再開した
- 再開時に、Claude Codeがそのworktreeを「本体とは別のチェックアウト」だと確認できなかった
Claude Codeは再開前にworktreeのgitメタデータを読み、本体と別のチェックアウトのままかを確かめます。この確認に落ちると、対話では隔離なしで続行し、-pとSDKでは再開そのものを中止します。
3種のエラーはどれを指すか
エラーは3種類あり、対話モードの通知と1対1で対応します。
| エラーの出だし | 対話側の通知 | startup_failure_reason | 紐づけ(binding) |
|---|---|---|---|
Error: cannot resume into worktree <path>: ... | 対話側の通知Did not re-enter your worktree | startup_failure_reasonworktree_resume_refused | 紐づけ(binding)解除してから終了 |
Error: could not verify worktree <path> for this resume, so the resume was aborted... | 対話側の通知Could not verify your worktree | startup_failure_reasonworktree_unverified | 紐づけ(binding)保持 |
Error: ...The worktree binding is kept. | 対話側の通知Could not re-enter your worktree | startup_failure_reasonworktree_resume_refused | 紐づけ(binding)保持 |
worktreeが消えている場合は例外で、Notice: the worktree <path> for this session no longer exists...と表示して、起動したディレクトリで続行します。これはエラーではなく通知です。
エラー文の末尾には、拒否の具体的な理由が付きます。この末尾は対話側の通知と共通なので、後述の直し方の表でそのまま引けます。
次の再開で何が起きるかは、紐づけの扱いで決まります。解除型のcannot resume into worktreeは2回目に隔離なしで通ります。保持型の2種は、could not verify worktreeなら再確認、The worktree binding is keptなら起動場所を変えない限り同じ結果です。
「cannot resume into worktree」は2回目に通る
cannot resume into worktreeは、止まる前に紐づけの解除を行います。そのため、同じコマンドを再実行すると今度は通ります。ただしそのときは、worktreeの隔離なしで現在のディレクトリのまま進みます。
これはv2.1.260以降の挙動です。v2.1.260より前は、解除した状態を書き込まなかったため、再試行しても毎回同じエラーで止まりました。古いバージョンのCIで「何度やっても同じ」になっていた場合は、まずバージョンを確かめてください。
claude --version自動で通るのは「隔離が外れた」ことの裏返しでもあります。2回目の実行は本体のチェックアウトを触れる状態なので、無人実行では1回目の失敗を握りつぶして再試行するより、原因を直してから再開するほうが安全です。
トランスクリプトの書き込みを止めていると同じ拒否が続く
紐づけの解除は、セッションのトランスクリプトに記録されます。書き込みを抑止していると、解除が保存されません。そのため再開のたびに同じ拒否が繰り返されます。
抑止する設定は2つあります。
CLAUDE_CODE_SKIP_PROMPT_HISTORY=1(全モードで書き込みを止める環境変数)claude -p --no-session-persistence(1回の非対話実行だけ書き込みを止めるフラグ)
この環境変数を付けて始めたセッションは、--resumeや--continueの一覧にも出なくなります。使い捨てのスクリプト実行向けの設定なので、再開を前提にした運用とは相性がよくありません。この状態で拒否されると、エラーは「同じコマンドはもう一度拒否される」と述べたうえで、--fork-sessionか新しい会話の開始を回避策に挙げます。
# 元のセッションのworktreeには触れず、現在のディレクトリで複製を始める
claude -p --resume <session-id> --fork-session "続きの作業をお願いします"--fork-sessionで再開した複製は、起動したディレクトリで始まります。元のセッションのworktreeは手つかずのままです。worktreeでの作業内容を引き継ぐ必要がなく、会話の文脈だけ続けたいときの逃がし方として使えます。
失敗の理由をスクリプトで分岐する
--output-format stream-jsonを付けると、拒否は標準出力にもresultメッセージとして届きます。subtypeはerror_during_executionで、errors配列にstderrと同じ文面が入ります。
claude -p --resume <session-id> --output-format stream-json --verbose \
"続きをお願いします" \
| jq -c 'select(.type == "result")
| {subtype, errors, startup_failure_reason}'startup_failure_reasonは、could not verify worktreeのときがworktree_unverified、cannot resume into worktreeとThe worktree binding is keptのときがworktree_resume_refusedです。文面を正規表現で拾うより、この値で分岐したほうが文言の変更に強くなります。例えば次のように分けられます。
worktree_unverifiedなら、一定時間後に同じ再開を再試行するworktree_resume_refusedなら、再試行せずに人へ通知する。紐づけが解除されるケースでは、2回目が隔離なしで通る点も判断材料にする
このフィールドはCLIではv2.1.274以降で付きます。v2.1.260より前の再開拒否では、resultメッセージ自体が出ませんでした。古い環境では、終了コードとstderrで判断する必要があります。
worktree_unverifiedとworktree_resume_refusedの2つは、CLAUDE_CODE_STARTUP_FAILURE_RESULTSを設定しなくても結果として出力されます。ほかの起動失敗の理由まで結果として受け取りたいときは、この環境変数に1を設定します(v2.1.274以降)。
Agent SDKで再開して失敗を受け取る
TypeScriptのAgent SDKでは、query()のoptions.resumeにセッションIDを渡すと再開できます。拒否された場合は例外にならず、typeがresultでsubtypeがerror_during_executionのメッセージが流れてきます。errors配列にはstderrと同じ文面が入り、startup_failure_reasonで原因を判別できます。
import { query } from "@anthropic-ai/claude-agent-sdk";
async function resumeOnce(sessionId: string, prompt: string) {
for await (const message of query({
prompt,
options: { resume: sessionId },
})) {
if (message.type !== "result") continue;
if (message.subtype === "success") return "done";
switch (message.startup_failure_reason) {
case "worktree_unverified":
return "retry-later"; // 紐づけは保持。時間を置いて同じ再開を試す
case "worktree_resume_refused":
console.error(message.errors.join("\n"));
return "needs-human"; // 末尾の理由を見て直す
default:
return "other-error"; // 別の失敗はerrorsを確認する
}
}
}startup_failure_reasonが付くのは、Agent SDKのv0.3.274以降です。この値を受け取る起動失敗のうち、worktreeに関わるworktree_unverifiedとworktree_resume_refusedは、追加の設定なしで結果として届きます。ほかの起動失敗の理由まで結果で受けたいときは、options.envにCLAUDE_CODE_STARTUP_FAILURE_RESULTSを1で渡します。
拒否の原因が直せない、またはトランスクリプトを書かない構成のときは、forkSession: trueを付けて再開します。元のセッションIDは変わらず、複製が新しいセッションIDで始まります。CLIの--fork-sessionでは複製が起動したディレクトリで始まるので、同じ考え方でworktreeの外で会話の文脈だけ続けたい場面に向きます。
for await (const message of query({
prompt: "続きの作業をお願いします",
options: { resume: sessionId, forkSession: true },
})) {
// 複製側のsession_idは、resultメッセージのsession_idから取れる
}SDKのforkSessionも、複製を新しいセッションIDで始める点はCLIの--fork-sessionと同じです。起動ディレクトリでの動きは、手元で一度試してから運用に載せてください。forkSessionはoptionsにあるブール値で、既定はfalseです。resumeと組み合わせたときだけ、元のセッションを続けずに新しいセッションIDへ分岐します。
再開前に複製だけ作りたいときは、SDKのforkSession()関数も使えます。セッションIDを渡すとトランスクリプトを新しいセッションへ写し、複製のセッションIDを返します。upToMessageIdを指定すれば、会話の途中までを写して分岐できます。返ったIDをresumeに渡すと複製側で続きを始められます。titleを省くと、元のセッションから付けた名前の末尾に(fork)が付きます。ただし、複製には元のファイルチェックポイントが含まれないため、分岐前に取ったチェックポイントへは巻き戻せません。forkSession()は、IDがUUIDでないとき、セッションが見つからないとき、メッセージが空のときに例外を投げるので、呼び出し側で受けてください。
起動失敗の結果は、トークンやコストの合計がゼロで届きます。課金や使用量の集計にこのメッセージを混ぜると数字が歪むので、startup_failure_reasonが付いたresultは集計の対象から外しておくと安全です。errorsの文面は、後述の表で末尾の理由を引く入力にもなります。ログにはerrorsをそのまま残しておくと、あとから原因を引き直せます。
末尾の理由から直し方を引く
エラー文の末尾に出る理由が、実際の直し方を決めます。Refusing to use <path> as an isolation worktreeの節に載っている対応を、再開エラー向けに並べ直すと次のとおりです。
| 末尾の文言 | 起きていること | 直し方 |
|---|---|---|
launch from the parent checkout / Run the resume from the project checkout | 起きていることworktreeの内側から起動した | 直し方本体のチェックアウトから起動し直す。worktreeの作り直しは不要 |
it cannot be resumed or re-entered | 起きていること起動した場所からworktreeを保証できる材料がない | 直し方worktreeを作り直す。親のチェックアウトがあれば、そこからの再開も有効 |
it contains the protected checkout | 起きていること拒否されたディレクトリが本体のチェックアウトの親(ホームなど) | 直し方そのディレクトリは消さず、worktreeのパスを変える |
the protected checkout <path> has a .git entry that could not be examined / has git metadata that could not be resolved | 起きていること本体の.git側に問題がある | 直し方worktreeは消さない。本体の権限やdubious ownershipを直して再試行する |
its recorded path has a network spelling | 起きていること記録されたパスがネットワークパス | 直し方ローカルのパスにworktreeを作り直す |
どの場合も、拒否されたディレクトリはClaude Codeが残します。作業内容があるかもしれないため、作り直す前に必要な変更を取り出してください。
worktreeに本体の.gitへの向きが混じる系の拒否は、別の症状でも出ます。作成時や入場時の拒否はClaude Codeのworktree isolation checksで拒否される原因と直し方で扱っています。
どこから切り分けるか
エラーを受けて最初に見る順序は次のとおりです。
再開エラーの切り分け
- Qworktreeのディレクトリは今も存在するか?答えが存在しない答えがはい
通知だけで再開は続行します。エラーではないので対処は不要です。
- Qworktreeの中のディレクトリから起動していないか?答えが内側から起動していない答えがはい
本体のチェックアウトから起動し直します。
- Qトランスクリプトの書き込みを抑止していないか?答えが抑止していない答えがはい
--fork-sessionか新しい会話に切り替えます。
末尾の理由で直す
上の表で、エラー文の末尾に一致する行の直し方を適用します。
対話の再開と何が違うのか
対話モードは通知を出して隔離なしで続行し、-pとSDKはresultで理由を返して止まります。対話側のメッセージ4種と再開時の挙動の一覧はClaude Codeでブランチをまたぐセッションはどう扱われるかにあります。
SDKからセッションを再開する基本(resume・continue・forkの使い分け)はAgent SDKのセッション管理が詳しい記事です。worktreeを使う運用全般はClaude Code Worktree実践ガイドにまとまっています。
まとめ
cannot resume into worktreeは、-pとAgent SDKの再開だけが取る慎重な挙動です。止まったときは、メッセージを3種に分けて、紐づけが解除されたか保持されたかを見ます。v2.1.260以降なら、解除型の拒否は2回目に通ります。ただし通った先は隔離のないディレクトリなので、無人実行ではstartup_failure_reasonで分岐して、人に回す判断を入れておくのが現実的です。トランスクリプトを書かない構成では、--fork-sessionが出口になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。