fix: prevent local:// URI from creating local: directory on Linux

On Linux, Node's path.normalize() collapses the double slash in
local://PLAN.md to local:/PLAN.md, creating a directory called local:
in the project root instead of routing through the local:// protocol handler.

Defense-in-depth fixes across 5 layers:

1. resolveToCwd() now throws if a path starts with any internal URL
   scheme prefix (local:, agent:, skill:, etc.), preventing all 59
   call sites from treating URIs as relative filesystem paths.

2. resolvePlanPath() now matches on local: prefix (not just local://)
   and normalizes local:/ to local:// before resolution, catching
   all slash variants.

3. Bash URL expansion regex and early-exit checks now also match
   local:/ (single slash), and normalize before resolution.

4. Edit preview/diff functions now gracefully skip internal URL paths
   instead of crashing via the resolveToCwd guard.

5. All startsWith('local://') checks updated to startsWith('local:')
   with normalization in agent-session, interactive-mode, and
   approved-plan modules.

Also adds local: to .gitignore to prevent accidental commits of the
leaked directory.
This commit is contained in:
djdembeck
2026-04-14 17:13:54 -05:00
committed by can1357
parent 2921332c44
commit 5da806671e
11 changed files with 62 additions and 23 deletions
+4 -1
View File
@@ -6,7 +6,7 @@
*/
import { isEnoent } from "@oh-my-pi/pi-utils";
import * as Diff from "diff";
import { resolveToCwd } from "../tools/path-utils";
import { isInternalUrlPath, resolveToCwd } from "../tools/path-utils";
import { DEFAULT_FUZZY_THRESHOLD, EditMatchError, findMatch } from "./modes/replace";
import { adjustIndentation, normalizeToLF, stripBom } from "./normalize";
@@ -761,6 +761,9 @@ export async function computeEditDiff(
if (oldText.length === 0) {
return { error: "oldText must not be empty." };
}
if (isInternalUrlPath(path)) {
return { error: `Preview not available for internal URL: ${path}` };
}
const absolutePath = resolveToCwd(path, cwd);
try {