From 947807036cbab12258a75eb344cfbfef23deb7cf Mon Sep 17 00:00:00 2001 From: weiconghe <13976098570@163.com> Date: Sat, 6 Jun 2026 18:31:11 +0800 Subject: [PATCH] fix: strip env variable prefixes from permission pattern matching The `source()` function returned the full command text including leading `variable_assignment` nodes (e.g., `GOFLAGS=-mod=vendor go test ./...`). Permission patterns like `go *` could not match, causing unnecessary permission prompts even when the rule should apply. Skip leading `variable_assignment` children in `source()` so the pattern becomes just the command portion (`go test ./...`), matching what `parts()` already does for the `always` auto-approval patterns. Closes #14110 Co-Authored-By: Claude Opus 4.7 --- packages/opencode/src/tool/shell.ts | 10 ++++- packages/opencode/test/tool/shell.test.ts | 52 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 7726c1bd496c..4d23bfb888a3 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -120,7 +120,15 @@ function parts(node: Node) { } function source(node: Node) { - return (node.parent?.type === "redirected_statement" ? node.parent.text : node.text).trim() + const target = node.parent?.type === "redirected_statement" ? node.parent : node + const text = target.text + let offset = 0 + for (let i = 0; i < target.childCount; i++) { + const child = target.child(i) + if (!child || child.type !== "variable_assignment") break + offset = child.endIndex - target.startIndex + } + return offset > 0 ? text.slice(offset).trim() : text.trim() } function commands(node: Node) { diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index d679fda1a8f3..ac3c7a23ebf0 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -239,6 +239,58 @@ describe("tool.shell permissions", () => { }), ) + for (const item of shells.filter((s) => !PS.has(s.label))) { + it.live(`strips env variable prefixes from permission pattern [${item.label}]`, () => + withShell( + item, + runIn( + projectRoot, + Effect.gen(function* () { + const requests: Array> = [] + yield* run( + { + command: "NODE_ENV=production echo hello", + description: "Echo with env var", + }, + capture(requests), + ) + expect(requests.length).toBe(1) + expect(requests[0].permission).toBe("bash") + expect(requests[0].patterns).toContain("echo hello") + expect(requests[0].patterns).not.toContain("NODE_ENV=production echo hello") + expect(requests[0].always).toContain("echo *") + }), + ), + ), + ) + } + + for (const item of shells.filter((s) => !PS.has(s.label))) { + it.live(`strips multiple env variable prefixes from permission pattern [${item.label}]`, () => + withShell( + item, + runIn( + projectRoot, + Effect.gen(function* () { + const requests: Array> = [] + yield* run( + { + command: "NODE_ENV=production DEBUG=1 go test ./...", + description: "Go test with env vars", + }, + capture(requests), + ) + expect(requests.length).toBe(1) + expect(requests[0].permission).toBe("bash") + expect(requests[0].patterns).toContain("go test ./...") + expect(requests[0].patterns).not.toContain("NODE_ENV=production DEBUG=1 go test ./...") + expect(requests[0].always).toContain("go test *") + }), + ), + ), + ) + } + each("asks for bash permission with multiple commands", () => Effect.gen(function* () { const tmp = yield* tmpdirScoped()