shadcn-ui/ui · error

A needsApproval tool call resolves from the user's decision;

Error message

A needsApproval tool call resolves from the user's decision; remove the ${method}() call.

What it means

In the chat writer, a tool created with needsApproval: true is resolved by the user's approve/deny decision, not by the script. assertResolvable() blocks calling output(), error(), or denied() on such a handle because the approval flow owns the resolution and supplies the result via the approval state.

Source

Thrown at packages/helpers/src/core/writer.ts:162

      if (clonedOptions.toolCallId !== undefined) {
        context.ids.reserveToolCallId(clonedOptions.toolCallId)
      }

      events.push({
        kind: "tool-input",
        name,
        toolCallId,
        title: clonedOptions.title,
        toolMetadata: clonedOptions.toolMetadata,
        providerExecuted: clonedOptions.providerExecuted,
        dynamic: clonedOptions.dynamic,
        input: clonedOptions.input ?? {},
      })

      function assertResolvable(method: string) {
        if (clonedOptions.needsApproval) {
          throw new Error(
            `A needsApproval tool call resolves from the user's decision; remove the ${method}() call.`
          )
        }
      }

      const handle: ToolHandle<TOOLS[NAME]["output"]> = {
        sleep(delayMs: number) {
          events.push({
            kind: "sleep",
            delayMs,
            phase: "after-start",
          })

          return handle
        },

        output(output: TOOLS[NAME]["output"]) {
          assertResolvable("output")

View on GitHub (pinned to efac598707)

Solutions

  1. Remove the .output()/.error()/.denied() call on needsApproval tool handles.
  2. Let the approval flow provide output via approval.output.
  3. If you must script the output yourself, set needsApproval: false.

Example fix

// before
writer.tool("deleteFile", { needsApproval: true, input }).output({ ok: true })

// after
writer.tool("deleteFile", { needsApproval: true, input })
// output is supplied by the user's approval decision
Defensive patterns

Strategy: validation

Validate before calling

// branch on needsApproval before scripting a resolution
const handle = writer.tool(name, options)
if (!options.needsApproval) {
  handle.output(result)
}

Type guard

const isApprovalTool = (
  opts: { needsApproval?: boolean }
): opts is { needsApproval: true } => Boolean(opts.needsApproval)

Prevention

When it happens

Trigger: Scripting writer.tool("x", { needsApproval: true }).output(...) (or .error()/.denied()) in the same turn; converting a normal tool to approval-gated without removing the explicit resolution call.

Common situations: Misunderstanding the approval lifecycle; copy-pasting a non-approval tool script and adding needsApproval.

Related errors


AI-assisted analysis of shadcn-ui/ui@efac598707 (2026-08-12). Data as JSON: /api/errors/bbba8941fecdb52d. Report an issue: GitHub.