Rejecting ExitPlanMode exits plan mode anyway
Bug: Rejecting ExitPlanMode exits plan mode anyway
Summary
When the user rejects an ExitPlanMode tool call, plan mode is deactivated as if it was approved. The model then makes edits freely on subsequent turns with no plan mode enforcement.
Steps to Reproduce
- Enter plan mode via
/plan - Let the model complete planning and write to the plan file
- When the model calls
ExitPlanMode, reject the tool call with a message like "I don't want to proceed with implementation" - On the next turn, send a request that would require edits (e.g., "fix the paths in that file")
- Observe: the model calls Edit/Write/Bash tools without restriction
Expected Behavior
Rejecting ExitPlanMode should keep plan mode active:
- The plan mode
<system-reminder>should continue to be injected on all subsequent turns - Edit, Write, Bash (non-readonly), and other mutating tools should remain blocked
- The model should only be able to edit the plan file
- The user should need to explicitly re-enter and exit plan mode to unlock edits
Actual Behavior
After rejecting ExitPlanMode:
- Plan mode
<system-reminder>tags stop appearing on subsequent turns - All tools (Edit, Write, Bash, etc.) are unblocked
- No error, warning, or indication that plan mode state changed
- The model proceeds to make edits as if plan mode was never active
Root Cause Hypothesis
The plan mode state machine appears to treat any ExitPlanMode tool call — regardless of whether the user approved or rejected it — as a state transition out of plan mode. The rejection handler likely shares the same exit path as the approval handler, or the state is toggled when the tool is called rather than when the tool call is accepted.
Specifically, the system-reminder injection logic (which gates plan mode enforcement) seems to check a flag that gets cleared on ExitPlanMode invocation rather than on ExitPlanMode acceptance. Since the model itself cannot enforce plan mode (it relies on the system-reminder being present to know it's in plan mode), removing the reminder effectively removes all enforcement.
Conversation Flow That Triggered This
Turn 1: [plan mode active, system-reminder present]
Model: writes plan file, calls ExitPlanMode
User: rejects ExitPlanMode ("I don't want to proceed")
Turn 2: [plan mode system-reminder ABSENT — this is the bug]
User: "fix the absolute paths"
Model: calls Edit tool — succeeds, no restriction
Turn 3: [still no system-reminder]
User: "create a new script"
Model: calls Write, Bash — succeeds
Turns 4-8: [model makes ~15 edits across multiple files]
No plan mode enforcement at any point
Turn 9: User notices and asks "how did you make changes in plan mode?"
Key Observations
- The model cannot self-enforce plan mode. It relies entirely on the
<system-reminder>injection. Once the reminder stops appearing, the model has no way to know it should still be in plan mode. - The bug persists across multiple turns — it's not a single-turn glitch.
- Re-entering plan mode later works correctly (system-reminders reappear), confirming the injection mechanism itself works — it's only the rejection-of-exit path that's broken.
- The plan file specified in the system-reminder remains accessible in later plan mode sessions, suggesting the plan file association is preserved even after the broken exit.
Suggested Fix
The ExitPlanMode rejection handler should:
- Not clear the plan mode flag — the state should remain
plan_mode: true - Continue injecting the plan mode system-reminder on subsequent turns
- Optionally: return a message to the model like "ExitPlanMode was rejected. You are still in plan mode." so the model can adjust its behavior
Severity
Medium-high. Plan mode is a trust/safety feature — users rely on it to review changes before they're made. When it silently deactivates, the model can make arbitrary file changes that the user didn't approve. In our case, ~15 file edits were made across 8 turns before the user noticed.
Environment
- Claude Code CLI (latest as of 2026-04-07)
- Model: Opus 4.6 (1M context)
- macOS
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗