BUG Cannot Exit Plan Mode In TypeScript SDK - Troubleshooting Guide
Introduction
Hey guys! Today, we're diving deep into a tricky bug encountered while using the TypeScript SDK with Claude Code's plan mode. This issue can be a real headache, causing an infinite loop when trying to exit plan mode. So, let's break down the problem, the steps to reproduce it, and how to potentially resolve it. Whether you're an experienced developer or just starting out, understanding these kinds of bugs is crucial for building robust applications. We'll go through the environment setup, the bug description, expected vs. actual behavior, and some additional context to give you a comprehensive view of the situation.
Environment
To get started, let's talk about the environment where this bug was observed. Knowing the specifics helps in replicating and fixing the issue. Here’s the setup:
- Platform: Anthropic API
- Claude CLI version: 1.0.59
- Operating System: macOS 15.5
- Terminal: iTerm2
This setup indicates that the issue is specific to the Anthropic API when using the TypeScript SDK. The Claude CLI version and the operating system might also play a role, but the core of the problem lies within the interaction between the SDK and the API in plan mode. Understanding the environment is the first step in debugging, as it helps narrow down the possible causes and contributing factors.
Bug Description
The core of the issue revolves around using the TypeScript SDK in plan
mode. Here’s a detailed breakdown of the bug:
- The
permissionMode
is set toplan
. This mode is designed to allow users to review and approve or deny actions before they are executed, providing a crucial layer of control and transparency. - A custom MCP (Manual Confirmation Protocol) server is used to handle tool permissions. The
mcpServers
is configured to point to this custom server, and a specific tool name (permissionPromptToolName
) is designated to manage permissions. This setup allows for fine-grained control over which tools can be used and when. - The custom-built HTTP server communicates with the app’s frontend, allowing users to accept or deny tools. This is a critical part of the workflow, as it ensures that users have the final say in what actions are taken.
- The problem arises when trying to allow the
exit_plan_mode
tool. While denying this tool works as expected, allowing it triggers an unexpected behavior. Claude Code immediately proceeds to implement the plan without changing thepermissionsMode
fromplan
. This is where the infinite loop begins.
This infinite loop occurs because Claude Code believes it is still in plan mode and needs to exit, even after the user has approved the exit. This creates a frustrating situation where the system gets stuck, unable to proceed further. The crux of the issue lies in how the permissionsMode
is handled after the exit_plan_mode
tool is approved.
Steps to Reproduce
To replicate this bug, follow these steps:
- Use the TypeScript SDK in
plan
mode. This is the starting point, setting the stage for the issue to occur. - Set up a local MCP server for handling tool permissions. This server is crucial for simulating the environment where the bug was observed.
- Approve a tool call to
exit_plan_mode
from Claude Code. This is the trigger that sets off the infinite loop.
By following these steps, you can reproduce the bug and gain a firsthand understanding of the problem. This is invaluable for debugging and finding a solution. Replicating the bug in a controlled environment allows for systematic testing and experimentation.
Expected Behavior
Now, let's discuss what should ideally happen when the exit_plan_mode
tool is approved. There are a couple of reasonable expectations:
- Return Control to the User: Claude Code should not generate a fake message on the user's behalf. Instead, it should return control to the user after the user approves
exit_plan_mode
. This would ensure transparency and maintain the user's control over the process. - Change the
permissionsMode
: More reasonably, thepermissionsMode
should be changed to something other thanplan
. Ideally, this would be customizable via an option in the SDK. This would prevent the infinite loop by signaling that the system has exited plan mode.
Both of these behaviors would address the core issue of the infinite loop. The first option prioritizes user control and transparency, while the second focuses on preventing the loop by correctly managing the permissionsMode
. A customizable option in the SDK would provide the most flexibility, allowing developers to choose the behavior that best fits their application's needs. Understanding the expected behavior is crucial for identifying the deviation and formulating a fix.
Actual Behavior
Unfortunately, the actual behavior deviates significantly from the expected behavior. Here’s what happens:
- Claude Code fakes a message on the user's behalf, approving the
exit_plan_mode
tool to the AI. This is problematic because it bypasses the user's direct input and creates a false state. - The
permissionsMode
is not changed, leaving Claude Code stuck in an endless loop. This is the crux of the issue, as the system continues to believe it is in plan mode, even after the user has supposedly approved the exit.
This behavior creates a critical flaw in the system, making it impossible to exit plan mode without manual intervention. The fake message further complicates the issue by obscuring the true state of the system and making it harder to debug. The combination of these two factors leads to a severe usability problem. Recognizing the actual behavior and how it differs from the expected behavior is essential for pinpointing the root cause of the bug.
Additional Context
To further illustrate the issue, let’s look at some relevant logs from the process running Claude Code and the MCP server:
[2025-07-23T21:16:22.762Z] MCPServer: Sending manual approval response: {
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"behavior\":\"allow\",\"updatedInput\":{\"plan\":\"...\"}}"
}
]
}
}
[2025-07-23T21:16:22.762Z] MCPServer: Response text content: {"behavior":"allow","updatedInput":{"plan":"..."}}
[2025-07-23T21:16:22.765Z] Received message 7: user [object Object]
[2025-07-23T21:16:22.765Z] Message 7 details: {
"type": "user",
"message": {
"role": "user",
"content": [
{
"type": "tool_result",
"content": "User has approved your plan. You can now start coding. Start with updating your todo list if applicable",
"tool_use_id": ...
}
]
},
"parent_tool_use_id": null,
"session_id": ...
}
Notice the user
message that our application never sent. This highlights that the Claude Code SDK is creating this message on our behalf, which is acceptable, but it fails to change the permissionsMode
. This log snippet provides concrete evidence of the issue, showing the discrepancy between the expected and actual behavior. Analyzing logs like these is a crucial part of the debugging process, as they offer valuable insights into the system's internal workings. This context helps in understanding the sequence of events leading to the bug and identifying the exact point of failure. Understanding the logs and the flow of messages is key to fixing this issue.
Possible Solutions and Workarounds
So, what can be done to fix this bug? Here are a few potential solutions and workarounds:
- Modify the SDK: The most direct solution would be to modify the TypeScript SDK to correctly handle the
permissionsMode
when theexit_plan_mode
tool is approved. This would involve changing the SDK's internal logic to ensure that thepermissionsMode
is updated to a value other thanplan
after the tool is used. This is the most robust solution, as it addresses the root cause of the problem. However, it requires access to the SDK's source code and a thorough understanding of its architecture. - Implement a Workaround in the MCP Server: Another approach is to implement a workaround in the MCP server. This could involve detecting when the
exit_plan_mode
tool is approved and manually triggering a change in thepermissionsMode
. While this is a less direct solution, it can be implemented without modifying the SDK. This might involve adding extra logic to the MCP server to monitor tool approvals and take appropriate actions. This is a viable option if modifying the SDK is not feasible. - Custom Logic to Intercept and Modify Messages: A more complex workaround might involve implementing custom logic to intercept and modify messages between the SDK and the MCP server. This could be used to prevent the fake user message from being sent or to force a change in the
permissionsMode
. This approach requires a deep understanding of the communication protocols between the SDK and the MCP server. It’s also more prone to errors and might introduce new issues if not implemented carefully.
Each of these solutions has its trade-offs. Modifying the SDK is the most effective but also the most challenging. The MCP server workaround is a good middle ground, while custom message interception is the most complex and risky. The best approach will depend on the specific constraints and resources available. Choosing the right solution is crucial for resolving the bug efficiently.
Conclusion
In conclusion, the bug encountered when attempting to exit plan mode in the TypeScript SDK is a significant issue that can lead to an infinite loop. This problem arises from the SDK's failure to correctly update the permissionsMode
after the exit_plan_mode
tool is approved. By understanding the environment, the steps to reproduce the bug, the expected and actual behavior, and the additional context provided by the logs, we can gain a comprehensive view of the issue. Several potential solutions and workarounds exist, ranging from modifying the SDK to implementing logic in the MCP server. The best approach will depend on the specific circumstances and resources available.
Remember, debugging is a process of investigation and experimentation. By systematically analyzing the problem and trying different solutions, we can overcome these challenges and build more robust and reliable systems. Whether you’re tackling this specific bug or facing other issues in your development journey, the key is to stay curious, keep learning, and never give up on finding a solution. Happy coding, guys!