Skip to content

Commit c0e96bd

Browse files
7418claude
andcommitted
fix: resolve spawn EINVAL on Windows — filter undefined env values and skip .cmd wrappers
Two root causes of spawn EINVAL on Windows: 1. process.env spread can include undefined values. Windows child_process.spawn requires all env values to be strings. sanitizeEnv now builds a clean object, dropping non-string entries. 2. Claude CLI installed via npm creates .cmd wrappers that cannot be spawned without shell:true. Detect .cmd/.bat extensions and fall back to SDK's internal resolution instead. Bump version to 0.10.7. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 83a0109 commit c0e96bd

File tree

3 files changed

+19
-8
lines changed

3 files changed

+19
-8
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.10.6",
3+
"version": "0.10.7",
44
"private": true,
55
"author": {
66
"name": "op7418",

src/lib/claude-client.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,18 @@ function sanitizeEnvValue(value: string): string {
3535

3636
/**
3737
* Sanitize all values in an env record so child_process.spawn won't
38-
* throw EINVAL due to invalid characters (null bytes, control chars).
38+
* throw EINVAL due to invalid characters or non-string values.
39+
* On Windows, spawn is strict: every env value MUST be a string.
40+
* Spreading process.env can include undefined values which cause EINVAL.
3941
*/
4042
function sanitizeEnv(env: Record<string, string>): Record<string, string> {
43+
const clean: Record<string, string> = {};
4144
for (const [key, value] of Object.entries(env)) {
4245
if (typeof value === 'string') {
43-
env[key] = sanitizeEnvValue(value);
46+
clean[key] = sanitizeEnvValue(value);
4447
}
4548
}
46-
return env;
49+
return clean;
4750
}
4851

4952
let cachedClaudePath: string | null | undefined;
@@ -291,10 +294,18 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
291294
queryOptions.allowDangerouslySkipPermissions = true;
292295
}
293296

294-
// Find claude binary for packaged app where PATH is limited
297+
// Find claude binary for packaged app where PATH is limited.
298+
// On Windows, npm installs Claude CLI as a .cmd wrapper which cannot
299+
// be spawned directly without shell:true — skip it and let the SDK
300+
// resolve the executable internally.
295301
const claudePath = findClaudePath();
296302
if (claudePath) {
297-
queryOptions.pathToClaudeCodeExecutable = claudePath;
303+
const ext = path.extname(claudePath).toLowerCase();
304+
if (ext === '.cmd' || ext === '.bat') {
305+
console.warn('[claude-client] Ignoring .cmd/.bat wrapper, falling back to SDK resolution:', claudePath);
306+
} else {
307+
queryOptions.pathToClaudeCodeExecutable = claudePath;
308+
}
298309
}
299310

300311
if (model) {

0 commit comments

Comments
 (0)