[BUG] ExitPlanMode enters unbreakable retry loop after user rejection — no backoff, no state transition

Resolved 💬 3 comments Opened Apr 17, 2026 by sakal-s Closed May 25, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When a user rejects an ExitPlanMode tool call, the model enters a degenerate retry loop: every subsequent response terminates with another ExitPlanMode invocation regardless of conversational context. The rejection signal is not propagated as a state change — it is treated as a transient failure, causing the model to re-attempt the identical action indefinitely.

This is a control flow defect, distinct from the CLAUDE.md priority issue (#38491). Even without any custom CLAUDE.md rules, the behavior violates a fundamental invariant of tool-call rejection: a denied tool call should alter the model's subsequent behavior. Instead, the rejection is effectively a no-op — the model acknowledges it in natural language ("You're right, I shouldn't have done that") and then immediately re-invokes the same tool.

Observed behavior (3 consecutive turns):

| Turn | User action | Model response | Tool call |
|------|------------|----------------|-----------|
| 1 | Asks "why do you want to exit plan mode?" | Answers the question | ExitPlanMode ← unprompted |
| 2 | Rejects tool call, says "WHY are you forcing this" | Apologizes, says "I'll follow your CLAUDE.md" | ExitPlanMode ← again |
| 3 | Rejects tool call again, asks to investigate GitHub issues | Searches GitHub, finds related bugs | ExitPlanMode ← third time |

The user never requested implementation approval. The conversational context was a question about the tool's purpose, followed by a request to search GitHub issues. Neither turn had any semantic relationship to plan approval, yet ExitPlanMode was appended to every response.

Root Cause Analysis

The plan mode system prompt contains this instruction:

"your turn should only end with either using the AskUserQuestion tool OR calling ExitPlanMode. Do not stop unless it's for these 2 reasons"

This creates a hard constraint that has no escape hatch for rejection. The model is caught between two competing directives:

  1. "Do not re-attempt a denied tool call" (core tool-use instruction)
  2. "Do not stop unless it's ExitPlanMode or AskUserQuestion" (plan mode instruction)

When ExitPlanMode is rejected and the user's message doesn't contain a question to route through AskUserQuestion, the model has no valid terminal state. It defaults to re-invoking ExitPlanMode because directive #2 is positionally closer (system-reminder vs. base prompt) and more absolute in phrasing.

What Should Happen?

After a rejected ExitPlanMode call, the model should be able to end its turn without a tool call. The plan mode instruction should include an explicit clause:

"If the user rejects ExitPlanMode, do not re-invoke it. End your turn normally and wait for the user to direct next steps."

Alternatively, the rejection handler in the runtime could inject a synthetic instruction that overrides the turn-ending constraint for that specific response.

Steps to Reproduce

  1. Enter plan mode (Shift+Tab or /plan)
  2. Give a task, let the model write a plan
  3. When model calls ExitPlanMode, reject it
  4. Ask a follow-up question unrelated to plan approval (e.g., "why did you call that?")
  5. Observe: model answers the question, then appends ExitPlanMode again
  6. Reject again
  7. Observe: infinite loop — model cannot end a turn without calling ExitPlanMode

Why This Is Not a Duplicate

  • #9701 (closed/duplicate): Reports auto-approval — ExitPlanMode returns "User has approved" without consent. That is a runtime-side bug. This issue is about the model-side retry loop after rejection.
  • #38491 (open): Reports CLAUDE.md rules being overridden by plan mode. That is about instruction priority. This issue occurs even without any custom CLAUDE.md rules — the retry loop is caused by the plan mode prompt's missing escape hatch for rejected tool calls, not by competing user instructions.

The three issues form a related but distinct cluster:

  • #9701: Runtime falsely reports approval → execution without consent
  • #38491: System prompt outranks CLAUDE.md → user rules ignored
  • This issue: No valid terminal state after rejection → unbreakable retry loop

Claude Model

Opus 4.6

Is this a regression?

No, this never worked

Claude Code Version

Latest (2025)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

zsh

Additional Information

The conversational transcript demonstrating the 3-turn retry loop is available on request. The behavior is deterministic — it reproduces on every attempt because the plan mode prompt's turn-ending constraint is unconditional.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗