Skip to content

preToolUse hook returning 'allow' should short-circuit built-in tool dialogs like exit_plan_mode #2349

@backnotprop

Description

@backnotprop

Summary

When a preToolUse hook returns permissionDecision: "allow" for the exit_plan_mode tool, the built-in plan approval dialog still appears, requiring the user to manually click through it. This makes it impossible for hooks to fully automate plan approval, even though "deny" already fully short-circuits tool execution.

Current behavior

  1. preToolUse hook fires for exit_plan_mode
  2. Hook returns { "permissionDecision": "allow" } -- this only answers "yes, this tool may run"
  3. The tool executes and calls requestExitPlanMode() internally
  4. The plan approval dialog (exit_plan_mode.requested) appears anyway
  5. User must manually approve a second time

"deny" works perfectly -- the tool never executes, no dialog appears. But "allow" is effectively a no-op for tools that have their own built-in interaction dialogs.

Use case

I build Plannotator, a visual plan review UI for agentic coding tools. My preToolUse hook intercepts exit_plan_mode, opens a rich review interface, collects the user's decision, and returns "allow" or "deny" with feedback. The deny path works great. But on approval, users still have to click through the CLI's plan dialog -- making the approval flow feel redundant.

Hook config:

{
  "hooks": {
    "preToolUse": [
      {
        "type": "command",
        "bash": "$HOME/.local/bin/plannotator copilot-plan",
        "timeoutSec": 345600
      }
    ]
  }
}

Proposed solution

Allow preToolUse hooks returning "allow" for tools with built-in dialogs (like exit_plan_mode) to supply the dialog response, skipping the dialog entirely. For example:

{
  "permissionDecision": "allow",
  "exitPlanModeResponse": {
    "approved": true,
    "selectedAction": "interactive",
    "autoApproveEdits": false
  }
}

Alternatively, any of these would also work:

  • A dedicated preExitPlanMode hook type in QueryHooks with ExitPlanModeResponse as its output type
  • A general preInteraction hook that fires before any blocking dialog on the InteractionManager
  • Treating preToolUse "allow" on exit_plan_mode as implicit plan approval (using defaults for selectedAction, etc.)

Why this matters

The current behavior is asymmetric: "deny" gives hooks full control, but "allow" still forces a manual step. Extensions that implement their own review UI (with richer context, annotations, sharing, etc.) cannot fully replace the built-in dialog on the approval path. This limits the extensibility that the hook system otherwise enables well.

For provenance purposes, this issue was AI assisted.

  • not spam though, also the person who authored this issue.

Metadata

Metadata

Assignees

No one assigned

    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