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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions