Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/adapters/vscode-copilot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { homedir } from "node:os";

import { CopilotBaseAdapter } from "../copilot-base.js";
import { resolveContextModeDataRoot } from "../base.js";
import { isVSCodeInstallPath } from "../../util/project-dir.js";
import type { CopilotHookInput, CopilotHookModule } from "../copilot-base.js";

import type {
Expand Down Expand Up @@ -77,10 +78,13 @@ export class VSCodeCopilotAdapter extends CopilotBaseAdapter {
// from this cascade — every direct VS Code Copilot session silently
// lost its workspace folder. PR #689 5-agent EM audit (Phase A
// claim verification) confirmed the gap; this is the minimal fix.
// Skipped when it points at VS Code's own install directory: that
// launch cwd is poisoned for remote workspaces.
// 3. process.cwd() — last resort.
const vscodeCwd = process.env.VSCODE_CWD;
return (
process.env.CLAUDE_PROJECT_DIR
|| process.env.VSCODE_CWD
|| (vscodeCwd && !isVSCodeInstallPath(vscodeCwd) ? vscodeCwd : undefined)
|| process.cwd()
);
}
Expand Down
17 changes: 17 additions & 0 deletions src/util/project-dir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ export function isPluginInstallPath(p: string): boolean {
return /[/\\]\.(claude|codex)[/\\]plugins[/\\](cache|marketplaces)[/\\]/.test(p);
}

/**
* Detect whether a path is VS Code's own application install directory
* rather than a real workspace folder.
*
* VS Code exports its launch cwd as VSCODE_CWD to spawned children. When the
* UI is launched from a shortcut and the workspace is remote, that cwd can
* be the local VS Code installation directory rather than the remote project.
*/
export function isVSCodeInstallPath(p: string): boolean {
if (!p) return false;
return (
/[/\\]Microsoft VS Code(?: - Insiders)?(?:[/\\]|$)/i.test(p) ||
/[/\\]Visual Studio Code(?: - Insiders)?(?:\.app)?(?:[/\\]|$)/i.test(p) ||
/[/\\](?:usr[/\\]share|opt)[/\\]code(?:-insiders)?(?:[/\\]|$)/i.test(p)
);
}

/**
* Read the per-session project dir from Claude Code's transcript files.
*
Expand Down
9 changes: 9 additions & 0 deletions tests/adapters/vscode-copilot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ describe("VSCodeCopilotAdapter", () => {
expect(event.projectDir).toBe("/vscode/workspace");
});

it("skips VSCODE_CWD when it points at the VS Code install path", () => {
delete process.env.CLAUDE_PROJECT_DIR;
process.env.VSCODE_CWD = "C:\\Users\\{userName}\\AppData\\Local\\Programs\\Microsoft VS Code";
const event = adapter.parsePreToolUseInput({
tool_name: "readFile",
});
expect(event.projectDir).toBe(process.cwd());
});

it("CLAUDE_PROJECT_DIR still wins over VSCODE_CWD when both are set (cascade order)", () => {
// Cascade order is locked: CLAUDE_PROJECT_DIR remains the top priority
// for users running VS Code under Claude Code's CLI; VSCODE_CWD is the
Expand Down
6 changes: 5 additions & 1 deletion tests/server-stdin-eof-exit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ setTimeout(() => {
});

try {
const result = await waitForClose(child, 2_000);
// The standalone bundle can take several seconds to initialize under
// WSL, especially when native dependencies are cold. The fatal handler
// still exits promptly after registration; allow startup time before
// classifying the child as surviving the exception storm.
const result = await waitForClose(child, 10_000);
if (!result.closed) {
child.kill("SIGTERM");
await waitForClose(child, 1_000);
Expand Down
25 changes: 25 additions & 0 deletions tests/util/project-dir.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join } from "node:path";
import { pathToFileURL } from "node:url";
import {
isPluginInstallPath,
isVSCodeInstallPath,
resolveProjectDir,
resolveProjectDirFromTranscript,
} from "../../src/util/project-dir.js";
Expand Down Expand Up @@ -97,6 +98,30 @@ describe("isPluginInstallPath", () => {
});
});

describe("isVSCodeInstallPath", () => {
it("matches Windows stable and Insiders install paths", () => {
expect(isVSCodeInstallPath("C:\\Users\\{userName}\\AppData\\Local\\Programs\\Microsoft VS Code")).toBe(true);
expect(isVSCodeInstallPath("C:\\Users\\{userName}\\AppData\\Local\\Programs\\Microsoft VS Code - Insiders")).toBe(true);
});

it("matches macOS app bundles", () => {
expect(isVSCodeInstallPath("/Applications/Visual Studio Code.app/Contents/Resources/app")).toBe(true);
expect(isVSCodeInstallPath("/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app")).toBe(true);
});

it("matches Linux stable and Insiders install paths", () => {
expect(isVSCodeInstallPath("/usr/share/code")).toBe(true);
expect(isVSCodeInstallPath("/usr/share/code-insiders")).toBe(true);
expect(isVSCodeInstallPath("/opt/code")).toBe(true);
expect(isVSCodeInstallPath("/opt/code-insiders")).toBe(true);
});

it("does not match an ordinary project containing code as a substring", () => {
expect(isVSCodeInstallPath("/home/{userName}/source/code-project")).toBe(false);
expect(isVSCodeInstallPath("C:\\Users\\{userName}\\workspace\\vscode-tools")).toBe(false);
});
});

describe("resolveProjectDir", () => {
it("returns the first non-plugin env var in priority order", () => {
const result = resolveProjectDir({
Expand Down