From 0c4f19865346c8e8e8ab9ad24e2c5acb5775d9e1 Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sat, 21 Feb 2026 14:21:00 -0800 Subject: [PATCH 1/2] fix: filter question tool and respect session permissions in non-interactive mode The question tool bypassed the permission pipeline by calling Question.ask() directly without going through ctx.ask(). Combined with LLM.resolveTools() only consulting agent-level permissions (where the build agent explicitly allows question), this caused opencode run to hang indefinitely when the model invoked the question tool (#11899). Three changes fix this: 1. LLM.resolveTools() now accepts session-level permissions and merges them with agent permissions when computing disabled tools, so session rules like question:deny actually filter the tool from the tool list. 2. SessionPrompt passes the session's permission ruleset through to the LLM layer via the new StreamInput.permission field. 3. The question tool now calls ctx.ask() before Question.ask(), so even if the tool isn't filtered out, the permission pipeline is consulted. On denial, it returns a helpful message instead of hanging. Refs #11899 Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/session/llm.ts | 8 ++++++-- packages/opencode/src/session/prompt.ts | 1 + packages/opencode/src/tool/question.ts | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 4e42fb0d2ec..427a1ad5274 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -39,6 +39,7 @@ export namespace LLM { tools: Record retries?: number toolChoice?: "auto" | "required" | "none" + permission?: PermissionNext.Ruleset } export type StreamOutput = StreamTextResult @@ -255,8 +256,11 @@ export namespace LLM { }) } - async function resolveTools(input: Pick) { - const disabled = PermissionNext.disabled(Object.keys(input.tools), input.agent.permission) + async function resolveTools(input: Pick) { + const disabled = PermissionNext.disabled( + Object.keys(input.tools), + PermissionNext.merge(input.agent.permission, input.permission ?? []), + ) for (const tool of Object.keys(input.tools)) { if (input.user.tools?.[tool] === false || disabled.has(tool)) { delete input.tools[tool] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 75bd3c9dfac..78548764599 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -674,6 +674,7 @@ export namespace SessionPrompt { tools, model, toolChoice: format.type === "json_schema" ? "required" : undefined, + permission: session.permission ?? [], }) // If structured output was captured, save it and exit immediately diff --git a/packages/opencode/src/tool/question.ts b/packages/opencode/src/tool/question.ts index a2887546d4b..7ee1680240b 100644 --- a/packages/opencode/src/tool/question.ts +++ b/packages/opencode/src/tool/question.ts @@ -1,6 +1,7 @@ import z from "zod" import { Tool } from "./tool" import { Question } from "../question" +import { PermissionNext } from "../permission/next" import DESCRIPTION from "./question.txt" export const QuestionTool = Tool.define("question", { @@ -9,6 +10,19 @@ export const QuestionTool = Tool.define("question", { questions: z.array(Question.Info.omit({ custom: true })).describe("Questions to ask"), }), async execute(params, ctx) { + try { + await ctx.ask({ permission: "question", patterns: ["*"], metadata: {}, always: ["*"] }) + } catch (e) { + if (e instanceof PermissionNext.DeniedError || e instanceof PermissionNext.RejectedError) { + return { + title: "Question denied", + output: "Cannot ask questions in this session. Make your best judgment and proceed.", + metadata: { answers: [] }, + } + } + throw e + } + const answers = await Question.ask({ sessionID: ctx.sessionID, questions: params.questions, From 4802c3332cb1532ad223cf0c89ba48558481f3f5 Mon Sep 17 00:00:00 2001 From: Jacob Morgan Date: Sat, 21 Feb 2026 14:21:10 -0800 Subject: [PATCH 2/2] fix: auto-approve ask permissions in opencode run The permission.asked event handler in run.ts auto-rejected all permission requests, causing any tool requiring "ask" permission (like file writes in certain directories) to fail. Users reported that opencode run would start a loop, run ls, then exit without doing useful work (#13851). Explicit "deny" rules still throw DeniedError before reaching the event handler, so changing the default reply from "reject" to "once" is safe -- it only affects "ask" rules that need interactive confirmation. A --no-auto-approve flag is added for CI environments where users want strict permission checking (original reject behavior). Refs #13851 Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/cli/cmd/run.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index f3781f1abd8..4710e80f570 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -297,6 +297,11 @@ export const RunCommand = cmd({ describe: "show thinking blocks", default: false, }) + .option("auto-approve", { + type: "boolean", + describe: "auto-approve permission requests (use --no-auto-approve to reject instead)", + default: true, + }) }, handler: async (args) => { let message = [...args.message, ...(args["--"] || [])] @@ -539,14 +544,15 @@ export const RunCommand = cmd({ if (event.type === "permission.asked") { const permission = event.properties if (permission.sessionID !== sessionID) continue + const reply = args["auto-approve"] ? "once" : "reject" UI.println( UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL + - `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`, + `permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-${reply === "once" ? "approving" : "rejecting"}`, ) await sdk.permission.reply({ requestID: permission.id, - reply: "reject", + reply, }) } }