From ea53b553c6603aee872249327be10a41c6b98aee Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 11 Dec 2025 17:15:49 +0100 Subject: [PATCH 1/4] feat: allow writes to /tmp by default without permission prompt Fixes #5386 Fixes #4743 --- packages/opencode/src/tool/bash.ts | 2 +- packages/opencode/src/tool/edit.ts | 2 +- packages/opencode/src/tool/patch.ts | 2 +- packages/opencode/src/tool/read.ts | 2 +- packages/opencode/src/tool/write.ts | 2 +- packages/opencode/src/util/filesystem.ts | 9 ++++++++- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 0c099fe8073..533f3372ec7 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -111,7 +111,7 @@ export const BashTool = Tool.define("bash", async () => { const agent = await Agent.get(ctx.agent) const checkExternalDirectory = async (dir: string) => { - if (Filesystem.contains(Instance.directory, dir)) return + if (Filesystem.isAllowedPath(Instance.directory, dir)) return const title = `This command references paths outside of ${Instance.directory}` if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index a5d34c949ff..310b9592a5e 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -42,7 +42,7 @@ export const EditTool = Tool.define("edit", { const agent = await Agent.get(ctx.agent) const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) - if (!Filesystem.contains(Instance.directory, filePath)) { + if (!Filesystem.isAllowedPath(Instance.directory, filePath)) { const parentDir = path.dirname(filePath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/patch.ts b/packages/opencode/src/tool/patch.ts index 93888f60bd2..fc0528ef644 100644 --- a/packages/opencode/src/tool/patch.ts +++ b/packages/opencode/src/tool/patch.ts @@ -53,7 +53,7 @@ export const PatchTool = Tool.define("patch", { for (const hunk of hunks) { const filePath = path.resolve(Instance.directory, hunk.path) - if (!Filesystem.contains(Instance.directory, filePath)) { + if (!Filesystem.isAllowedPath(Instance.directory, filePath)) { const parentDir = path.dirname(filePath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index 27426ad2412..f8ed0df84bb 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -30,7 +30,7 @@ export const ReadTool = Tool.define("read", { const title = path.relative(Instance.worktree, filepath) const agent = await Agent.get(ctx.agent) - if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) { + if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.isAllowedPath(Instance.directory, filepath)) { const parentDir = path.dirname(filepath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 7b109261eb1..3d00575ae52 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -21,7 +21,7 @@ export const WriteTool = Tool.define("write", { const agent = await Agent.get(ctx.agent) const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) - if (!Filesystem.contains(Instance.directory, filepath)) { + if (!Filesystem.isAllowedPath(Instance.directory, filepath)) { const parentDir = path.dirname(filepath) if (agent.permission.external_directory === "ask") { await Permission.ask({ diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index a3dcfc70367..119007de79d 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,7 +1,14 @@ import { exists } from "fs/promises" -import { dirname, join, relative } from "path" +import { dirname, join, normalize, relative } from "path" +import { tmpdir } from "os" export namespace Filesystem { + const systemTmpDir = normalize(tmpdir()) + + export function isAllowedPath(projectDir: string, filepath: string) { + const normalized = normalize(filepath) + return contains(projectDir, normalized) || contains(systemTmpDir, normalized) || contains("/tmp", normalized) + } export function overlaps(a: string, b: string) { const relA = relative(a, b) const relB = relative(b, a) From 1648ef11425fd83ad8ad91c74b8ff671203bc4b8 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 11 Dec 2025 17:24:07 +0100 Subject: [PATCH 2/4] fix: resolve /tmp symlink on macOS to /private/tmp --- packages/opencode/src/util/filesystem.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 119007de79d..e70eaf5a12f 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,13 +1,19 @@ -import { exists } from "fs/promises" +import { exists, realpath } from "fs/promises" import { dirname, join, normalize, relative } from "path" import { tmpdir } from "os" export namespace Filesystem { const systemTmpDir = normalize(tmpdir()) + // on macOS /tmp is a symlink to /private/tmp, resolve it + const tmpDirResolved = await realpath("/tmp").catch(() => null) export function isAllowedPath(projectDir: string, filepath: string) { const normalized = normalize(filepath) - return contains(projectDir, normalized) || contains(systemTmpDir, normalized) || contains("/tmp", normalized) + if (contains(projectDir, normalized)) return true + if (contains(systemTmpDir, normalized)) return true + if (contains("/tmp", normalized)) return true + if (tmpDirResolved && contains(tmpDirResolved, normalized)) return true + return false } export function overlaps(a: string, b: string) { const relA = relative(a, b) From 266ddb8498b4562c90bc65fb94b14d0f34ae86e2 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 11 Dec 2025 17:26:02 +0100 Subject: [PATCH 3/4] fix: use sync realpath for module-level initialization --- packages/opencode/src/util/filesystem.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index e70eaf5a12f..beab1a98139 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -1,11 +1,18 @@ import { exists, realpath } from "fs/promises" +import { realpathSync } from "fs" import { dirname, join, normalize, relative } from "path" import { tmpdir } from "os" export namespace Filesystem { const systemTmpDir = normalize(tmpdir()) // on macOS /tmp is a symlink to /private/tmp, resolve it - const tmpDirResolved = await realpath("/tmp").catch(() => null) + const tmpDirResolved = (() => { + try { + return realpathSync("/tmp") + } catch { + return null + } + })() export function isAllowedPath(projectDir: string, filepath: string) { const normalized = normalize(filepath) From cd202b6e3a70b5324f3b1cbf207aca8a13bebf7f Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Thu, 11 Dec 2025 18:07:53 +0100 Subject: [PATCH 4/4] test: update external directory test to use /usr instead of /tmp --- packages/opencode/test/tool/bash.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 9ef7dfb9d8f..b8ccc34f772 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -376,8 +376,8 @@ describe("tool.bash permissions", () => { bash.execute( { command: "ls", - workdir: "/tmp", - description: "List /tmp", + workdir: "/usr", + description: "List /usr", }, ctx, ),