summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-01-04 01:49:31 -0600
committerAiden Cline <[email protected]>2026-01-04 01:49:49 -0600
commit36112604050c5578fef8833a2d2c0c85e1382219 (patch)
tree63c11127b1bb9eca2cc27d30956be9db906279bb
parentc3fd3c865681eb4fb27b0b3070f9cc96a4afa287 (diff)
downloadopencode-36112604050c5578fef8833a2d2c0c85e1382219.tar.gz
opencode-36112604050c5578fef8833a2d2c0c85e1382219.zip
core: remove hardcoded .env read block and use new permissions model instead
-rw-r--r--packages/opencode/src/agent/agent.ts7
-rw-r--r--packages/opencode/src/tool/read.ts16
-rw-r--r--packages/opencode/test/tool/read.test.ts53
3 files changed, 42 insertions, 34 deletions
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index db49b0f4f..c683727df 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -47,6 +47,13 @@ export namespace Agent {
"*": "allow",
doom_loop: "ask",
external_directory: "ask",
+ // mirrors github.com/github/gitignore Node.gitignore pattern for .env files
+ read: {
+ "*": "allow",
+ "*.env": "deny",
+ "*.env.*": "deny",
+ "*.env.example": "allow",
+ },
})
const user = PermissionNext.fromConfig(cfg.permission ?? {})
diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts
index 847fe3ebe..a0f50129e 100644
--- a/packages/opencode/src/tool/read.ts
+++ b/packages/opencode/src/tool/read.ts
@@ -8,7 +8,6 @@ import DESCRIPTION from "./read.txt"
import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance"
import { Identifier } from "../id/id"
-import { iife } from "@/util/iife"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -47,21 +46,6 @@ export const ReadTool = Tool.define("read", {
metadata: {},
})
- const block = iife(() => {
- const basename = path.basename(filepath)
- const whitelist = [".env.sample", ".env.example", ".example", ".env.template"]
-
- if (whitelist.some((w) => basename.endsWith(w))) return false
- // Block .env, .env.local, .env.production, etc. but not .envrc
- if (/^\.env(\.|$)/.test(basename)) return true
-
- return false
- })
-
- if (block) {
- throw new Error(`The user has blocked you from reading ${filepath}, DO NOT make further attempts to read it`)
- }
-
const file = Bun.file(filepath)
if (!(await file.exists())) {
const dir = path.dirname(filepath)
diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts
index 1093a17fe..826fa03f6 100644
--- a/packages/opencode/test/tool/read.test.ts
+++ b/packages/opencode/test/tool/read.test.ts
@@ -3,7 +3,8 @@ import path from "path"
import { ReadTool } from "../../src/tool/read"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
-import type { PermissionNext } from "../../src/permission/next"
+import { PermissionNext } from "../../src/permission/next"
+import { Agent } from "../../src/agent/agent"
const ctx = {
sessionID: "test",
@@ -122,29 +123,45 @@ describe("tool.read external_directory permission", () => {
})
describe("tool.read env file blocking", () => {
- test.each([
+ const cases: [string, boolean][] = [
[".env", true],
[".env.local", true],
[".env.production", true],
- [".env.sample", false],
+ [".env.development.local", true],
[".env.example", false],
[".envrc", false],
["environment.ts", false],
- ])("%s blocked=%s", async (filename, blocked) => {
- await using tmp = await tmpdir({
- init: (dir) => Bun.write(path.join(dir, filename), "content"),
- })
- await Instance.provide({
- directory: tmp.path,
- fn: async () => {
- const read = await ReadTool.init()
- const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctx)
- if (blocked) {
- await expect(promise).rejects.toThrow("blocked")
- } else {
- expect((await promise).output).toContain("content")
- }
- },
+ ]
+
+ describe.each(["build", "plan"])("agent=%s", (agentName) => {
+ test.each(cases)("%s blocked=%s", async (filename, blocked) => {
+ await using tmp = await tmpdir({
+ init: (dir) => Bun.write(path.join(dir, filename), "content"),
+ })
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const agent = await Agent.get(agentName)
+ const ctxWithPermissions = {
+ ...ctx,
+ ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
+ for (const pattern of req.patterns) {
+ const rule = PermissionNext.evaluate(req.permission, pattern, agent.permission)
+ if (rule.action === "deny") {
+ throw new PermissionNext.DeniedError(agent.permission)
+ }
+ }
+ },
+ }
+ const read = await ReadTool.init()
+ const promise = read.execute({ filePath: path.join(tmp.path, filename) }, ctxWithPermissions)
+ if (blocked) {
+ await expect(promise).rejects.toThrow(PermissionNext.DeniedError)
+ } else {
+ expect((await promise).output).toContain("content")
+ }
+ },
+ })
})
})
})