diff --git a/src/hooks/prometheus-md-only/index.test.ts b/src/hooks/prometheus-md-only/index.test.ts index ac0c93c9..71e31aa0 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 5a0d7f99..d5839e81 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 }