From 38169523c4678cba553f3f6d271689cc0e5912c2 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 7 Feb 2026 18:49:16 +0900 Subject: [PATCH] fix: anchor .sisyphus path check to ctx.directory to prevent false positives - Uses path.join(ctx.directory, '.sisyphus') + sep as prefix instead of loose .includes() - Prevents false positive when .sisyphus exists in parent directories outside project root - Adds test for the false positive case (cubic review feedback) --- bun.lock | 28 +++++++++---------- .../write-existing-file-guard/index.test.ts | 19 +++++++++++++ src/hooks/write-existing-file-guard/index.ts | 5 ++-- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index b2412a61..c65491b0 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,13 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.2.3", - "oh-my-opencode-darwin-x64": "3.2.3", - "oh-my-opencode-linux-arm64": "3.2.3", - "oh-my-opencode-linux-arm64-musl": "3.2.3", - "oh-my-opencode-linux-x64": "3.2.3", - "oh-my-opencode-linux-x64-musl": "3.2.3", - "oh-my-opencode-windows-x64": "3.2.3", + "oh-my-opencode-darwin-arm64": "3.2.4", + "oh-my-opencode-darwin-x64": "3.2.4", + "oh-my-opencode-linux-arm64": "3.2.4", + "oh-my-opencode-linux-arm64-musl": "3.2.4", + "oh-my-opencode-linux-x64": "3.2.4", + "oh-my-opencode-linux-x64-musl": "3.2.4", + "oh-my-opencode-windows-x64": "3.2.4", }, }, }, @@ -226,19 +226,19 @@ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.2.3", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Doc9xQCj5Jmx3PzouBIfvDwmfWM94Y9Q9IngFqOjrVpfBef9V/WIH0PlhJU6ps4BKGey8Nf2afFq3UE06Z63Hg=="], + "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.2.4", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-6vG49R/nkbZYhAqN2oStA+8reZRo2KPPHSbhQd4htdEpzS4ipVz6pW/YTj/TDwunQO7hy66AhP9hOR4pJcoDeA=="], - "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.2.3", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-w7lO0Hn/AlLCHe33KPbje83Js2h5weDWVMuopEs6d3pi/1zkRDBEhCi63S4J0d0EKod9kEPQA6ojtdVJ4J39zQ=="], + "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.2.4", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Utfpclg8xHj93+faX2L4dpkzhM6D58YEtjkVlHq4CxZ8MdpYCs2l4NtY/b9T1GWmtQWFxZQhmIdAcwe1qApgpQ=="], - "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.2.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-m1tS1jRLO2Svm5NuetK3BAgdAR8b2GkiIfMFoIYsLJTPmzIkXaigAYkFq+BXCs5JAbRmPmvjndz9cuCddnPADQ=="], + "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.2.4", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-z4Zlvt1a1PSQVprbgx6bLOeNuILX4d9p80GrTWuuYzqY+OEgbb74LVVUFCsvt8UgnhRTnHuhmphSpIL7UznzZg=="], - "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.2.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Q/0AGtOuUFGNGIX8F6iD5W8c2spbjrqVBPt0B7laQSwnScKs/BI+TvM6HRE37vhoWg+fzhAX3QYJ2H9Un9FYrg=="], + "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.2.4", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-pCCPM8rsuwMR3a7XIDyYyr/D1HkMPffOYGXeOY8vBaLL8NKFl8d0H5twA3HIiEqcDINHV3kw9zteL2paW+mHSQ=="], - "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.2.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-RIAyoj2XbT8vH++5fPUkdO+D1tfqxh+iWto7CqWr1TgbABbBJljGk91HJgS9xjnxyCQJEpFhTmO7NMHKJcZOWQ=="], + "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.2.4", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vU9l4rS1oRpCgyXalBiUOOFPddIwSmuWoGY1PgO4dr6Db+gtEpmaDpLcEi5j4jFUDRLH6btQvNAp/eAydVgOJQ=="], - "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.2.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-nnQK3y7R4DrBvqdqRGbujL2oAAQnVVb23JHUbJPQ6YxrRRGWpLOVGvK5c16ykSFEUPl8eZDmi1ON/R4opKLOUw=="], + "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.2.4", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OZ+yRl7tOXoWTHh7zQ8WsTasKqZaIaVO3QeUQhDIS5JXFjbgjMgFeC/XBegsCgfqglWTOlMatmCO1S3nx2vy2w=="], - "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.2.3", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-mt8E/TkpaCp04pvzwntT8x8TaqXDt3zCD5X2eA8ZZMrb5ofNr5HyG5G4SFXrUh+Ez3b/3YXpNWv6f6rnAlk1Dg=="], + "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.2.4", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-W6TX8OiPCOmu7UZgZESh5DSWat0zH/6WPC3tdvjzwYnik9ZvRiyJGHh9B4uAG3DdqTC+pZJrpuTq1NctqMJiDA=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], diff --git a/src/hooks/write-existing-file-guard/index.test.ts b/src/hooks/write-existing-file-guard/index.test.ts index f9654b64..ff5aba1f 100644 --- a/src/hooks/write-existing-file-guard/index.test.ts +++ b/src/hooks/write-existing-file-guard/index.test.ts @@ -267,6 +267,25 @@ describe("createWriteExistingFileGuardHook", () => { await expect(result).rejects.toThrow("File already exists. Use edit tool instead.") }) + test("blocks write when .sisyphus is in parent path but not under ctx.directory", async () => { + //#given + const fakeSisyphusParent = path.join(os.tmpdir(), ".sisyphus", "evil-project") + fs.mkdirSync(fakeSisyphusParent, { recursive: true }) + const evilFile = path.join(fakeSisyphusParent, "plan.md") + fs.writeFileSync(evilFile, "# Evil Plan") + const input = { tool: "Write", sessionID: "ses_1", callID: "call_1" } + const output = { args: { filePath: evilFile, content: "# Hacked" } } + + //#when + const result = hook["tool.execute.before"]?.(input as any, output as any) + + //#then + await expect(result).rejects.toThrow("File already exists. Use edit tool instead.") + + // cleanup + fs.rmSync(path.join(os.tmpdir(), ".sisyphus"), { recursive: true, force: true }) + }) + test("blocks write to existing regular file (not in .sisyphus)", async () => { //#given const regularFile = path.join(tempDir, "regular.md") diff --git a/src/hooks/write-existing-file-guard/index.ts b/src/hooks/write-existing-file-guard/index.ts index f3a52240..1256b241 100644 --- a/src/hooks/write-existing-file-guard/index.ts +++ b/src/hooks/write-existing-file-guard/index.ts @@ -1,6 +1,6 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { existsSync } from "fs" -import { resolve, isAbsolute } from "path" +import { resolve, isAbsolute, join, sep } from "path" import { log } from "../../shared" export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks { @@ -20,7 +20,8 @@ export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks { const resolvedPath = isAbsolute(filePath) ? filePath : resolve(ctx.directory, filePath) if (existsSync(resolvedPath)) { - const isSisyphusMarkdown = resolvedPath.replace(/\\/g, "/").includes("/.sisyphus/") && filePath.endsWith(".md") + const sisyphusRoot = join(ctx.directory, ".sisyphus") + sep + const isSisyphusMarkdown = resolvedPath.startsWith(sisyphusRoot) && resolvedPath.endsWith(".md") if (isSisyphusMarkdown) { log("[write-existing-file-guard] Allowing .sisyphus/*.md overwrite", { sessionID: input.sessionID,