From 49b0b5e0858adee27fd506338626d1ded00be1a1 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 11 Jan 2026 15:27:51 +0900 Subject: [PATCH] fix(prometheus-md-only): allow nested project paths with .sisyphus directory Use regex /\.sisyphus[/\\]/i instead of checking first path segment. This fixes Windows paths where ctx.directory is parent of the actual project (e.g., project\.sisyphus\drafts\...). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- src/hooks/prometheus-md-only/index.test.ts | 63 ++++++++++++++++++++-- src/hooks/prometheus-md-only/index.ts | 8 +-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index ac0c93c96..71e31aa0b 100644 --- a/src/hooks/prometheus-md-only/index.test.ts +++ b/src/hooks/prometheus-md-only/index.test.ts @@ -373,8 +373,8 @@ describe("prometheus-md-only", () => { ).rejects.toThrow("can only write/edit .md files inside .sisyphus/") }) - test("should block nested .sisyphus directories", async () => { - // #given + test("should allow nested .sisyphus directories (ctx.directory may be parent)", async () => { + // #given - when ctx.directory is parent of actual project, path includes project name const hook = createPrometheusMdOnlyHook(createMockPluginInput()) const input = { tool: "Write", @@ -385,10 +385,10 @@ describe("prometheus-md-only", () => { args: { filePath: "src/.sisyphus/plans/x.md" }, } - // #when / #then + // #when / #then - should allow because .sisyphus is in path await expect( hook["tool.execute.before"](input, output) - ).rejects.toThrow("can only write/edit .md files inside .sisyphus/") + ).resolves.toBeUndefined() }) test("should block path traversal attempts", async () => { @@ -426,5 +426,60 @@ describe("prometheus-md-only", () => { hook["tool.execute.before"](input, output) ).resolves.toBeUndefined() }) + + test("should allow nested project path with .sisyphus (Windows real-world case)", async () => { + // #given - simulates when ctx.directory is parent of actual project + // User reported: xauusd-dxy-plan\.sisyphus\drafts\supabase-email-templates.md + const hook = createPrometheusMdOnlyHook(createMockPluginInput()) + const input = { + tool: "Write", + sessionID: TEST_SESSION_ID, + callID: "call-1", + } + const output = { + args: { filePath: "xauusd-dxy-plan\\.sisyphus\\drafts\\supabase-email-templates.md" }, + } + + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) + + test("should allow nested project path with mixed separators", async () => { + // #given + const hook = createPrometheusMdOnlyHook(createMockPluginInput()) + const input = { + tool: "Write", + sessionID: TEST_SESSION_ID, + callID: "call-1", + } + const output = { + args: { filePath: "my-project/.sisyphus\\plans/task.md" }, + } + + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).resolves.toBeUndefined() + }) + + test("should block nested project path without .sisyphus", async () => { + // #given + const hook = createPrometheusMdOnlyHook(createMockPluginInput()) + const input = { + tool: "Write", + sessionID: TEST_SESSION_ID, + callID: "call-1", + } + const output = { + args: { filePath: "my-project\\src\\code.ts" }, + } + + // #when / #then + await expect( + hook["tool.execute.before"](input, output) + ).rejects.toThrow("can only write/edit .md files") + }) }) }) diff --git a/src/hooks/prometheus-md-only/index.ts b/src/hooks/prometheus-md-only/index.ts index 5a0d7f990..d5839e815 100644 --- a/src/hooks/prometheus-md-only/index.ts +++ b/src/hooks/prometheus-md-only/index.ts @@ -14,6 +14,7 @@ export * from "./constants" * - Mixed separators (e.g., .sisyphus\\plans/x.md) * - Case-insensitive directory/extension matching * - Workspace confinement (blocks paths outside root or via traversal) + * - Nested project paths (e.g., parent/.sisyphus/... when ctx.directory is parent) */ function isAllowedFile(filePath: string, workspaceRoot: string): boolean { // 1. Resolve to absolute path @@ -27,10 +28,9 @@ function isAllowedFile(filePath: string, workspaceRoot: string): boolean { return false } - // 4. Split by both separators and check first segment matches ALLOWED_PATH_PREFIX (case-insensitive) - // Guard: if rel is empty (filePath === workspaceRoot), segments[0] would be "" — reject - const segments = rel.split(/[/\\]/) - if (!segments[0] || segments[0].toLowerCase() !== ALLOWED_PATH_PREFIX.toLowerCase()) { + // 4. Check if .sisyphus/ or .sisyphus\ exists anywhere in the path (case-insensitive) + // This handles both direct paths (.sisyphus/x.md) and nested paths (project/.sisyphus/x.md) + if (!/\.sisyphus[/\\]/i.test(rel)) { return false }