{"record":{"id":"bbba8941fecdb52d","repo":"shadcn-ui/ui","slug":"a-needsapproval-tool-call-resolves-from-the-user-s","errorCode":null,"errorMessage":"A needsApproval tool call resolves from the user's decision; remove the ${method}() call.","messagePattern":"A needsApproval tool call resolves from the user's decision; remove the (.+?)\\(\\) call\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/helpers/src/core/writer.ts","lineNumber":162,"sourceCode":"\n      if (clonedOptions.toolCallId !== undefined) {\n        context.ids.reserveToolCallId(clonedOptions.toolCallId)\n      }\n\n      events.push({\n        kind: \"tool-input\",\n        name,\n        toolCallId,\n        title: clonedOptions.title,\n        toolMetadata: clonedOptions.toolMetadata,\n        providerExecuted: clonedOptions.providerExecuted,\n        dynamic: clonedOptions.dynamic,\n        input: clonedOptions.input ?? {},\n      })\n\n      function assertResolvable(method: string) {\n        if (clonedOptions.needsApproval) {\n          throw new Error(\n            `A needsApproval tool call resolves from the user's decision; remove the ${method}() call.`\n          )\n        }\n      }\n\n      const handle: ToolHandle<TOOLS[NAME][\"output\"]> = {\n        sleep(delayMs: number) {\n          events.push({\n            kind: \"sleep\",\n            delayMs,\n            phase: \"after-start\",\n          })\n\n          return handle\n        },\n\n        output(output: TOOLS[NAME][\"output\"]) {\n          assertResolvable(\"output\")","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/shadcn-ui/ui/blob/efac5987074af84ece57c367c6dd83387b967022/packages/helpers/src/core/writer.ts#L144-L180","documentation":"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.","triggerScenarios":"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.","commonSituations":"Misunderstanding the approval lifecycle; copy-pasting a non-approval tool script and adding needsApproval.","solutions":["Remove the .output()/.error()/.denied() call on needsApproval tool handles.","Let the approval flow provide output via approval.output.","If you must script the output yourself, set needsApproval: false."],"exampleFix":"// before\nwriter.tool(\"deleteFile\", { needsApproval: true, input }).output({ ok: true })\n\n// after\nwriter.tool(\"deleteFile\", { needsApproval: true, input })\n// output is supplied by the user's approval decision","handlingStrategy":"validation","validationCode":"// branch on needsApproval before scripting a resolution\nconst handle = writer.tool(name, options)\nif (!options.needsApproval) {\n  handle.output(result)\n}","typeGuard":"const isApprovalTool = (\n  opts: { needsApproval?: boolean }\n): opts is { needsApproval: true } => Boolean(opts.needsApproval)","tryCatchPattern":null,"preventionTips":["Never chain output/error/denied on a needsApproval tool handle.","Document per-tool whether it is approval-gated."],"tags":["chat","tools","approval","writer"],"backgroundTag":null,"analyzedSha":"efac5987074af84ece57c367c6dd83387b967022","analyzedAt":"2026-08-12T05:00:50.218Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}