From 2c97b987448c424497572456f8d5a59a8abd5ca4 Mon Sep 17 00:00:00 2001 From: jiangxt <971760512@qq.com> Date: Sat, 10 Oct 2026 14:11:03 +0800 Subject: [PATCH 1/2] fix(roots): expand home paths in resolveAllowedPath `resolveAllowedPath` resolved its input against the working directory without expanding a leading `~`, while `assertAllowedPath` expanded it. The two disagreed: a path such as `~/.codex/skills/caveman/SKILL.md` became `/~/.codex/skills/caveman/SKILL.md` instead of resolving against the home directory. `open_workspace` advertises skill entrypoints as `~`-prefixed paths, so every skill read failed with ENOENT. The skill read fallback in `resolveReadPath` only runs when workspace path resolution throws, and the literal `~` path resolved silently inside the workspace, so the fallback never fired. --- src/roots.test.ts | 15 +++++++++++++-- src/roots.ts | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/roots.test.ts b/src/roots.test.ts index 90bdaa243..c8fe90f74 100644 --- a/src/roots.test.ts +++ b/src/roots.test.ts @@ -20,9 +20,20 @@ assert.equal( resolve(home, "personal", "devspace"), ); +// Home expansion applies before working-directory resolution, so a `~` path +// resolves against the home directory rather than becoming a literal `~` +// directory inside the workspace. assert.equal( - resolveAllowedPath("~/file.txt", "/workspace", ["/workspace"]), - resolve("/workspace", "~/file.txt"), + resolveAllowedPath("~/personal/devspace", "/workspace", [join(home, "personal")]), + resolve(home, "personal", "devspace"), +); + +// A `~` path outside the allowed roots is denied instead of being silently +// mapped inside the workspace. Skill reads rely on this: the denial lets the +// read fall through to the skill-path resolver. +assert.throws( + () => resolveAllowedPath("~/file.txt", "/workspace", ["/workspace"]), + /Path is outside allowed roots/, ); if (process.platform === "win32") { diff --git a/src/roots.ts b/src/roots.ts index 284726f49..c681f1cc3 100644 --- a/src/roots.ts +++ b/src/roots.ts @@ -42,7 +42,7 @@ export function assertAllowedPath(path: string, allowedRoots: string[]): string } export function resolveAllowedPath(inputPath: string, cwd: string, allowedRoots: string[]): string { - const absolutePath = resolve(cwd, inputPath); + const absolutePath = resolve(cwd, expandHomePath(inputPath)); return assertAllowedPath(absolutePath, allowedRoots); } From e63d772be7bc49bc096eb6767ae75007fde58549 Mon Sep 17 00:00:00 2001 From: linfeline <32232133+linfeline@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:43:17 +0800 Subject: [PATCH 2/2] test(roots): make outside-root assertion HOME-independent --- src/roots.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/roots.test.ts b/src/roots.test.ts index c8fe90f74..655e528ca 100644 --- a/src/roots.test.ts +++ b/src/roots.test.ts @@ -32,7 +32,10 @@ assert.equal( // mapped inside the workspace. Skill reads rely on this: the denial lets the // read fall through to the skill-path resolver. assert.throws( - () => resolveAllowedPath("~/file.txt", "/workspace", ["/workspace"]), + () => { + const workspace = resolve(home, "workspace"); + return resolveAllowedPath("~/file.txt", workspace, [workspace]); + }, /Path is outside allowed roots/, );