fix(vscode, cli): handle Windows cross-drive paths in ignore checks (#7479)

On Windows, path.relative() returns an absolute path when source and
target are on different drives. The ignore npm package throws a
RangeError when fed such paths. This caused 'Failed to send prompt'
when VS Code had open tabs from another drive (e.g. extension settings
in AppData while workspace is on D:).

Guard all path.relative() → ignore.ignores() call sites against
absolute results by checking path.isAbsolute() and a Windows
drive-letter regex.
This commit is contained in:
Marius
2026-03-25 11:17:52 +01:00
committed by GitHub
parent fcc0bc792b
commit 6bb5776e5d
4 changed files with 69 additions and 4 deletions
+2 -2
View File
@@ -2471,7 +2471,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const uri = tab.input.uri
if (uri.scheme === "file") {
const rel = path.relative(dir, uri.fsPath)
if (!rel.startsWith("..") && controller.validateAccess(uri.fsPath)) {
if (!rel.startsWith("..") && !path.isAbsolute(rel) && controller.validateAccess(uri.fsPath)) {
result.add(rel.replaceAll("\\", "/"))
}
}
@@ -2505,7 +2505,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return undefined
}
const relative = path.relative(workspaceDir, fsPath)
if (relative.startsWith("..")) {
if (relative.startsWith("..") || path.isAbsolute(relative)) {
return undefined
}
return relative
@@ -10,6 +10,11 @@ const GITIGNORE = ".gitignore"
*/
const SENSITIVE_PATTERNS = [".env", ".env.*"]
// Matches Windows drive-letter absolute paths (e.g. "C:/" or "c:\").
// path.isAbsolute() on POSIX does not recognise these, so we check explicitly
// to avoid passing them to the `ignore` package which throws a RangeError.
const WINDOWS_DRIVE = /^[a-zA-Z]:[/\\]/
function toPosix(filePath: string): string {
return filePath.replace(/\\/g, "/")
}
@@ -87,7 +92,7 @@ export class FileIgnoreController {
}
const relative = path.relative(this.workspacePath, resolved)
if (!relative || relative.startsWith("..")) {
if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || WINDOWS_DRIVE.test(relative)) {
return null
}
@@ -2,8 +2,16 @@ import { afterEach, describe, expect, it } from "bun:test"
import os from "node:os"
import path from "node:path"
import fs from "node:fs/promises"
import ignore from "ignore"
import { FileIgnoreController } from "../../src/services/autocomplete/shims/FileIgnoreController"
// Activate Windows drive-letter detection in the `ignore` package.
// On actual Windows this runs automatically (process.platform === 'win32');
// here we enable it explicitly so the test reproduces the Windows-only
// RangeError on any platform.
const setup = (ignore as any)[Symbol.for("setupWindows")]
if (typeof setup === "function") setup()
const tempDirs: string[] = []
afterEach(async () => {
@@ -102,6 +110,55 @@ describe("FileIgnoreController", () => {
})
})
describe("Windows cross-drive paths", () => {
it("does not throw for a Windows-style absolute path from another drive", async () => {
const workspace = await createTempWorkspace()
await fs.writeFile(path.join(workspace, ".gitignore"), "node_modules/\n")
const controller = new FileIgnoreController(workspace)
await controller.initialize()
// Simulates a VS Code tab open on a file from a different Windows drive.
// On Windows, path.relative("D:\\project", "C:\\Users\\file") returns
// "C:\\Users\\file" (absolute), which the `ignore` package rejects via
// RangeError: path should be a `path.relative()`d string.
//
// On macOS, path.resolve joins "c:/..." relative to the workspace,
// producing "c:/Users/..." as the relative portion — still detected as
// a Windows drive letter by ignore's setupWindows() regex.
const cross =
"c:/Users/User/AppData/Roaming/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json"
expect(() => controller.validateAccess(cross)).not.toThrow()
expect(controller.validateAccess(cross)).toBe(false)
})
it("does not throw for file:// URIs with Windows drive letters", async () => {
const workspace = await createTempWorkspace()
await fs.writeFile(path.join(workspace, ".gitignore"), "node_modules/\n")
const controller = new FileIgnoreController(workspace)
await controller.initialize()
const uri =
"file:///c:/Users/User/AppData/Roaming/Code/User/globalStorage/kilocode.kilo-code/settings/mcp_settings.json"
expect(() => controller.validateAccess(uri)).not.toThrow()
expect(controller.validateAccess(uri)).toBe(false)
})
it("still allows workspace files after cross-drive check", async () => {
const workspace = await createTempWorkspace()
await fs.writeFile(path.join(workspace, ".gitignore"), "node_modules/\n")
const controller = new FileIgnoreController(workspace)
await controller.initialize()
expect(controller.validateAccess(path.join(workspace, "src", "main.ts"))).toBe(true)
expect(controller.validateAccess(path.join(workspace, "node_modules", "foo.js"))).toBe(false)
})
})
describe("when constructed with empty workspace path", () => {
it("denies all access", async () => {
const controller = new FileIgnoreController("")
+4 -1
View File
@@ -596,12 +596,15 @@ export namespace File {
const fullPath = path.join(resolved, entry.name)
const relativePath = path.relative(Instance.directory, fullPath)
const type = entry.isDirectory() ? "directory" : "file"
// On Windows, path.relative() across drives returns an absolute path;
// skip the gitignore check in that case to avoid a RangeError from `ignore`.
const canIgnore = !path.isAbsolute(relativePath)
nodes.push({
name: entry.name,
path: relativePath,
absolute: fullPath,
type,
ignored: ignored(type === "directory" ? relativePath + "/" : relativePath),
ignored: canIgnore && ignored(type === "directory" ? relativePath + "/" : relativePath),
})
}
return nodes.sort((a, b) => {