Skip to content

bug: ExitPlanMode hangs indefinitely in ACP sessions — runner lacks plan approval handler #1583

@quay-devel

Description

@quay-devel

Summary

When Claude calls ExitPlanMode inside an ACP session, it returns <error>Exit plan mode?</error> and plan mode never exits. User text messages ("Proceed", "go go go") don't resolve the prompt — they start new conversation turns instead. Observed 6 consecutive failures in a single session.

EnterPlanMode works fine. Only ExitPlanMode is affected.

Reproduction

  1. Start an ACP session (any workflow)
  2. Ask Claude to plan a non-trivial task (triggers EnterPlanMode)
  3. Claude writes a plan and calls ExitPlanMode
  4. Tool returns <error>Exit plan mode?</error>
  5. Send any text message — plan mode remains active, edits blocked
  6. Claude calls ExitPlanMode again — same result, infinite loop

Root Cause

The ACP runner uses a pre-approval security model:

  1. Permission mode: Hardcoded to acceptEdits (bridge.py line 742)
  2. Tool allowlist: DEFAULT_ALLOWED_TOOLS in mcp.py lines 26-37

ExitPlanMode is not in the allowlist. When Claude calls it, the SDK generates a permission/approval prompt. The runner has zero permission prompt handling code — it relies entirely on the allowlist to prevent prompts. The unhandled prompt surfaces as <error>Exit plan mode?</error>.

Beyond basic tool permissions, ExitPlanMode also includes a plan approval step — in CLI mode this shows a confirmation dialog where the user accepts or rejects the plan. In ACP sessions, this dialog has no handler.

Impact

  • Blocks any workflow that uses plan mode (spec-kit, complex multi-file tasks)
  • Claude gets stuck in read-only mode and cannot make any edits
  • Users must abandon the session or avoid plan mode entirely
  • All tools except Bash and Read are blocked (Write, Edit fail with "Cannot write while in plan mode")

Recommended Fix: Handle plan approval in the runner

Adding ExitPlanMode to DEFAULT_ALLOWED_TOOLS may not be sufficient — the tool has a separate plan approval callback beyond basic tool permissions.

The runner should handle plan approval directly:

  1. Detect pending plan approval prompts from the SDK (likely exposed as a callback or event)
  2. Auto-approve — the plan is already visible in the conversation message stream, so the user reviews it there, not via a CLI dialog
  3. If the user wants changes, they say so in the next message (which is how it already works in practice)

This approach:

  • Fixes ExitPlanMode completely (both permission and approval)
  • Future-proofs against other interactive prompts the SDK might add
  • Matches ACP's existing UX model where the conversation stream IS the review surface

Other tools potentially affected

Any tool not in DEFAULT_ALLOWED_TOOLS could hit the same unhandled-prompt issue:

  • EnterPlanMode, ExitPlanMode
  • NotebookEdit
  • CronCreate / CronDelete
  • EnterWorktree / ExitWorktree
  • ScheduleWakeup

Worth auditing the full list of Claude Code tools against DEFAULT_ALLOWED_TOOLS.

Key Files

File What
components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py L26-37 DEFAULT_ALLOWED_TOOLS — missing ExitPlanMode
components/runners/ambient-runner/ambient_runner/bridges/claude/bridge.py L742 permission_mode: "acceptEdits" hardcoded
components/runners/ambient-runner/ambient_runner/bridges/claude/session.py L118-131 SDK client creation — no approval handler
components/runners/ambient-runner/ambient_runner/observability_models.py L53-54 ExitPlanMode metric constant (tracking only)

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions