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>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-13 16:48:59 +08:00
1 parent 83a0109af1
commit c0e96bd1aa
3 files changed
+19 -8

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "codepilot",
"version": "0.10.6",
"version": "0.10.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codepilot",
"version": "0.10.6",
"version": "0.10.7",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.33",
"@hugeicons/core-free-icons": "^3.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "codepilot",
"version": "0.10.6",
"version": "0.10.7",
"private": true,
"author": {
"name": "op7418",
+16 -5
View File
@@ -35,15 +35,18 @@ function sanitizeEnvValue(value: string): string {
/**
* Sanitize all values in an env record so child_process.spawn won't
* throw EINVAL due to invalid characters (null bytes, control chars).
* throw EINVAL due to invalid characters or non-string values.
* On Windows, spawn is strict: every env value MUST be a string.
* Spreading process.env can include undefined values which cause EINVAL.
*/
function sanitizeEnv(env: Record<string, string>): Record<string, string> {
const clean: Record<string, string> = {};
for (const [key, value] of Object.entries(env)) {
if (typeof value === 'string') {
env[key] = sanitizeEnvValue(value);
clean[key] = sanitizeEnvValue(value);
}
}
return env;
return clean;
}
let cachedClaudePath: string | null | undefined;
@@ -291,10 +294,18 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
queryOptions.allowDangerouslySkipPermissions = true;
}
// Find claude binary for packaged app where PATH is limited
// Find claude binary for packaged app where PATH is limited.
// On Windows, npm installs Claude CLI as a .cmd wrapper which cannot
// be spawned directly without shell:true — skip it and let the SDK
// resolve the executable internally.
const claudePath = findClaudePath();
if (claudePath) {
queryOptions.pathToClaudeCodeExecutable = claudePath;
const ext = path.extname(claudePath).toLowerCase();
if (ext === '.cmd' || ext === '.bat') {
console.warn('[claude-client] Ignoring .cmd/.bat wrapper, falling back to SDK resolution:', claudePath);
} else {
queryOptions.pathToClaudeCodeExecutable = claudePath;
}
}
if (model) {