Skip to content

Commit bb67d18

Browse files
IM.codesclaude
andcommitted
fix(conpty): use absolute cmd.exe path and normalize CWD slashes on Windows
When the daemon launches via a Windows Scheduled Task or Startup shortcut, the restricted environment cannot resolve 'cmd.exe' through PATH, causing CreateProcess to fail with 'File not found' and all sessions to enter the error state with a runaway restart loop. Fix: resolve cmd.exe via COMSPEC env var (always set by Windows), falling back to %SystemRoot%\system32\cmd.exe. Also convert CWD backslashes to forward slashes to avoid node-pty's internal path.resolve error 267 on some versions. Tests: 3 new cases for COMSPEC resolution, fallback path, and CWD slash normalization. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 010150e commit bb67d18

File tree

2 files changed

+73
-3
lines changed

2 files changed

+73
-3
lines changed

src/agent/conpty.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,18 @@ export async function conptyNewSession(
138138

139139
const cols = opts?.cols ?? 200;
140140
const rows = opts?.rows ?? 50;
141-
// Normalize cwd: forward slashes → backslashes on Windows (node-pty's CreateProcess requires native paths)
141+
// Normalize cwd: backslashes → forward slashes. node-pty on some versions calls
142+
// path.resolve() internally which can fail with error 267 (ERROR_DIRECTORY) when
143+
// passed Windows-style backslash paths.
142144
const rawCwd = opts?.cwd ?? process.cwd();
143-
const cwd = process.platform === 'win32' ? rawCwd.replace(/\//g, '\\') : rawCwd;
145+
const cwd = process.platform === 'win32' ? rawCwd.replace(/\\/g, '/') : rawCwd;
146+
147+
// Use absolute path to cmd.exe — when the daemon is launched via a Windows
148+
// Scheduled Task or Startup shortcut the environment may be restricted and
149+
// CreateProcess cannot resolve a bare 'cmd.exe' through PATH.
150+
const cmdExe = process.platform === 'win32'
151+
? (process.env.COMSPEC ?? `${process.env.SystemRoot ?? 'C:\\Windows'}\\system32\\cmd.exe`)
152+
: 'cmd.exe';
144153

145154
// Strip redundant cwdPrefix from the command string.
146155
// Drivers prepend `cd /d "C:\path" && ` or `cd "path" && ` for tmux/wezterm,
@@ -151,7 +160,7 @@ export async function conptyNewSession(
151160
cleanCmd = cleanCmd.slice(cdMatch[0].length);
152161
}
153162

154-
const pty = spawn('cmd.exe', ['/c', cleanCmd], {
163+
const pty = spawn(cmdExe, ['/c', cleanCmd], {
155164
cwd,
156165
env: process.platform === 'win32'
157166
? buildWindowsEnv(opts?.env)

test/agent/conpty.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,67 @@ describe('conpty backend', () => {
171171
cwd: expect.any(String),
172172
}));
173173
});
174+
175+
// ── Windows cmd.exe path resolution (regression for "File not found" on restricted launch) ──
176+
177+
it('uses COMSPEC absolute path on Windows instead of bare cmd.exe', async () => {
178+
const origPlatform = process.platform;
179+
const origComspec = process.env.COMSPEC;
180+
Object.defineProperty(process, 'platform', { value: 'win32' });
181+
process.env.COMSPEC = 'C:\\Windows\\system32\\cmd.exe';
182+
183+
try {
184+
await conpty.conptyNewSession('comspec-test', 'claude --help', { cwd: 'C:/Users/admin' });
185+
const spawnedExe = spawnMock.mock.calls.at(-1)?.[0] as string;
186+
expect(spawnedExe).toBe('C:\\Windows\\system32\\cmd.exe');
187+
// Must NOT use the bare name that fails in restricted environments
188+
expect(spawnedExe).not.toBe('cmd.exe');
189+
} finally {
190+
Object.defineProperty(process, 'platform', { value: origPlatform });
191+
if (origComspec === undefined) delete process.env.COMSPEC;
192+
else process.env.COMSPEC = origComspec;
193+
}
194+
});
195+
196+
it('falls back to SystemRoot\\system32\\cmd.exe when COMSPEC is unset on Windows', async () => {
197+
const origPlatform = process.platform;
198+
const origComspec = process.env.COMSPEC;
199+
const origSystemRoot = process.env.SystemRoot;
200+
Object.defineProperty(process, 'platform', { value: 'win32' });
201+
delete process.env.COMSPEC;
202+
process.env.SystemRoot = 'C:\\Windows';
203+
204+
try {
205+
await conpty.conptyNewSession('fallback-cmd', 'echo hi', { cwd: 'C:/tmp' });
206+
const spawnedExe = spawnMock.mock.calls.at(-1)?.[0] as string;
207+
expect(spawnedExe).toBe('C:\\Windows\\system32\\cmd.exe');
208+
} finally {
209+
Object.defineProperty(process, 'platform', { value: origPlatform });
210+
if (origComspec === undefined) delete process.env.COMSPEC;
211+
else process.env.COMSPEC = origComspec;
212+
if (origSystemRoot === undefined) delete process.env.SystemRoot;
213+
else process.env.SystemRoot = origSystemRoot;
214+
}
215+
});
216+
217+
it('normalizes backslash CWD to forward slashes on Windows (avoids node-pty error 267)', async () => {
218+
const origPlatform = process.platform;
219+
const origComspec = process.env.COMSPEC;
220+
Object.defineProperty(process, 'platform', { value: 'win32' });
221+
process.env.COMSPEC = 'C:\\Windows\\system32\\cmd.exe';
222+
223+
try {
224+
await conpty.conptyNewSession('cwd-slash', 'claude', { cwd: 'C:\\Users\\admin\\project' });
225+
const spawnedCwd = spawnMock.mock.calls.at(-1)?.[2]?.cwd as string;
226+
// Backslashes must be converted to forward slashes
227+
expect(spawnedCwd).toBe('C:/Users/admin/project');
228+
expect(spawnedCwd).not.toContain('\\');
229+
} finally {
230+
Object.defineProperty(process, 'platform', { value: origPlatform });
231+
if (origComspec === undefined) delete process.env.COMSPEC;
232+
else process.env.COMSPEC = origComspec;
233+
}
234+
});
174235
});
175236

176237
describe('conptySessionExists / conptyListSessions', () => {

0 commit comments

Comments
 (0)