Skip to content

fix(acpx): make Claude CLI spawn robust on Windows#51366

Closed
ShionEria wants to merge 1 commit intoopenclaw:mainfrom
ShionEria:fix/acpx-windows-spawn-50352
Closed

fix(acpx): make Claude CLI spawn robust on Windows#51366
ShionEria wants to merge 1 commit intoopenclaw:mainfrom
ShionEria:fix/acpx-windows-spawn-50352

Conversation

@ShionEria
Copy link
Copy Markdown
Contributor

Summary

  • normalize Windows Claude CLI launch so paths with spaces are handled correctly
  • keep Unix behavior unchanged
  • add regression coverage for Windows command construction

Testing

  • not run in this temp environment because network access to GitHub/npm was blocked during the original heartbeat run

Fixes #50352

@openclaw-barnacle openclaw-barnacle Bot added extensions: acpx size: S r: too-many-prs Auto-close: author has more than twenty active PRs. labels Mar 21, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c3a4dc0a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +180 to +183
const child = spawn(target.command, target.args, {
stdio: ["pipe", "pipe", "inherit"],
env: process.env,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wrap Windows .cmd/.bat agent commands before spawning

On Windows hosts where targetCommand resolves to a wrapper script such as ...\\claude.cmd --stdio or npx.cmd, this still hands the wrapper directly to child_process.spawn(). The repo already documents that direct .cmd/.bat spawns are rejected on modern Node (src/process/exec.ts:44-47) and routes them through cmd.exe or a resolved entrypoint elsewhere, so these installations will still fail to start the Claude agent even though the new parser now accepts those paths.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 21, 2026

Greptile Summary

This PR fixes a real Windows bug where Claude CLI paths containing spaces (e.g. C:\Program Files\Claude\claude.exe) were being split on whitespace, yielding a broken command. It addresses two complementary scenarios: unquoted Windows paths (via a new WINDOWS_EXECUTABLE_PATH_RE regex that extracts the full executable path before any arguments) and quoted Windows paths (by fixing splitCommandParts to treat \X inside double-quoted strings as a literal backslash unless X is " or \). The isMainModule() guard is a clean addition that enables the file to be imported in tests without triggering side effects.

  • Core fix is correct for the primary use case (claude.exe paths with spaces, both quoted and unquoted).
  • .cmd/.bat/.ps1 extensions are included in the regex, but spawning these file types requires shell: true or explicit interpreter invocation on Windows; the spawn call is unchanged and uses shell: false.
  • Argument values containing unquoted Windows paths (e.g. --input C:\Users\data.json) will be silently corrupted by splitCommandParts, since that function still treats unquoted \ as an escape character. This is unlikely to affect the Claude CLI flags in practice, but the gap is undocumented.
  • Test coverage is good for the two headline cases, but the double-escaped backslash scenario that quoteCommandPart actually produces in the live code path is not covered.

Confidence Score: 3/5

  • Safe for the Claude CLI use case, but contains an untested live code path and a spawning issue with shell-script extensions.
  • The primary fix (handling .exe paths with spaces) is logically correct and well-tested. However, there are two non-trivial concerns: (1) .cmd/.bat/.ps1 entries in the regex suggest support for shell-script targets, but the spawn call will fail for these without shell: true; (2) unquoted Windows paths in argument values passed after the executable are silently mangled by splitCommandParts. Additionally, the test suite does not cover the double-escaped backslash case that quoteCommandPart produces at runtime, leaving a gap between the test input shapes and the actual live data.
  • Pay close attention to extensions/acpx/src/runtime-internals/mcp-proxy.mjs — specifically the WINDOWS_EXECUTABLE_PATH_RE extension list (lines 8-9) and the argument parsing after the command is extracted (lines 88-94).
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/acpx/src/runtime-internals/mcp-proxy.mjs
Line: 8-9

Comment:
**`.cmd`/`.bat` files cannot be spawned directly without `shell: true`**

The `WINDOWS_EXECUTABLE_PATH_RE` regex includes `cmd`, `bat`, and `ps1` as recognized Windows executable extensions, but the `spawn` call uses `shell: false` (the default). On Windows, `.cmd`, `.bat`, and `.ps1` files are not executable binaries — they require the shell (`cmd.exe` or `powershell.exe`) to interpret them. Spawning one of these directly will result in an `ENOENT` or `UNKNOWN` error at runtime.

If these extensions are genuinely expected to be valid target commands, the `spawn` call should conditionally set `shell: true` (or pass via `cmd /c`/`powershell -File`) when the resolved command ends with one of these extensions.

If they are only included in the regex as a future-proofing measure but aren't currently supported as target commands, they should be removed from the regex to avoid giving users a false sense that they'll work.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/acpx/src/runtime-internals/mcp-proxy.mjs
Line: 88-94

Comment:
**Unquoted Windows path with backslashes in arguments is silently corrupted**

When `splitWindowsExecutableCommand` successfully extracts the command, `rest` is passed to `splitCommandParts`. If `rest` contains an unquoted Windows path (e.g. a `--input C:\Users\data.json` argument), `splitCommandParts` is still in Unix-mode and will treat the `\` as an escape character — consuming it and yielding `C:Usersdata.json` silently.

For example:
```
C:\Program Files\Claude\claude.exe --config C:\Users\me\cfg.json
                                             ^^^^^^^^^^^^^^^^^^ parsed by splitCommandParts
```
would produce `args: ["--config", "C:Usersmecfg.json"]`.

In practice this is unlikely to affect the current Claude CLI use case (`--stdio`, `--dangerously-skip-permissions`, etc. are plain flags), but it is a silent correctness gap. Consider documenting that argument values containing Windows paths must be quoted, or apply a similar regex-aware treatment to the `rest` portion.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/acpx/src/runtime-internals/mcp-agent-command.test.ts
Line: 61-71

Comment:
**No test for double-backslash sequences inside quoted Windows paths**

`splitCommandParts` now treats `\\` inside double-quoted strings as a single `\` (escape sequence), while `\X` (any non-special char) is kept as a literal `\X`. The test only exercises the literal case. A test for the escaped-backslash round-trip (i.e. the path that `quoteCommandPart` in `mcp-agent-command.ts` would actually produce — double-escaped backslashes) is missing:

```ts
it("parses double-escaped backslashes inside quoted Windows paths", () => {
  // quoteCommandPart produces "C:\\\\Program Files\\\\Claude\\\\claude.exe"
  const parsed = splitCommandLine(
    '"C:\\\\Program Files\\\\Claude\\\\claude.exe" --stdio',
    "win32",
  );
  expect(parsed).toEqual({
    command: "C:\\Program Files\\Claude\\claude.exe",
    args: ["--stdio"],
  });
});
```

Without this test, a regression in `\\` handling could go undetected even though it would break the live code path (since the payload always stores the output of `quoteCommandPart`, which doubles the backslashes).

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix(acpx): make Clau..."

Comment on lines +8 to +9
const WINDOWS_EXECUTABLE_PATH_RE =
/^(?<command>(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+[\\/]).*?\.(?:exe|cmd|bat|com|ps1|cjs|mjs|js))(?=\s|$)(?:\s+(?<rest>.*))?$/i;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 .cmd/.bat files cannot be spawned directly without shell: true

The WINDOWS_EXECUTABLE_PATH_RE regex includes cmd, bat, and ps1 as recognized Windows executable extensions, but the spawn call uses shell: false (the default). On Windows, .cmd, .bat, and .ps1 files are not executable binaries — they require the shell (cmd.exe or powershell.exe) to interpret them. Spawning one of these directly will result in an ENOENT or UNKNOWN error at runtime.

If these extensions are genuinely expected to be valid target commands, the spawn call should conditionally set shell: true (or pass via cmd /c/powershell -File) when the resolved command ends with one of these extensions.

If they are only included in the regex as a future-proofing measure but aren't currently supported as target commands, they should be removed from the regex to avoid giving users a false sense that they'll work.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/acpx/src/runtime-internals/mcp-proxy.mjs
Line: 8-9

Comment:
**`.cmd`/`.bat` files cannot be spawned directly without `shell: true`**

The `WINDOWS_EXECUTABLE_PATH_RE` regex includes `cmd`, `bat`, and `ps1` as recognized Windows executable extensions, but the `spawn` call uses `shell: false` (the default). On Windows, `.cmd`, `.bat`, and `.ps1` files are not executable binaries — they require the shell (`cmd.exe` or `powershell.exe`) to interpret them. Spawning one of these directly will result in an `ENOENT` or `UNKNOWN` error at runtime.

If these extensions are genuinely expected to be valid target commands, the `spawn` call should conditionally set `shell: true` (or pass via `cmd /c`/`powershell -File`) when the resolved command ends with one of these extensions.

If they are only included in the regex as a future-proofing measure but aren't currently supported as target commands, they should be removed from the regex to avoid giving users a false sense that they'll work.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +88 to +94
return null;
}
const rest = match.groups.rest?.trim() ?? "";
return {
command: match.groups.command,
args: rest ? splitCommandParts(rest) : [],
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unquoted Windows path with backslashes in arguments is silently corrupted

When splitWindowsExecutableCommand successfully extracts the command, rest is passed to splitCommandParts. If rest contains an unquoted Windows path (e.g. a --input C:\Users\data.json argument), splitCommandParts is still in Unix-mode and will treat the \ as an escape character — consuming it and yielding C:Usersdata.json silently.

For example:

C:\Program Files\Claude\claude.exe --config C:\Users\me\cfg.json
                                             ^^^^^^^^^^^^^^^^^^ parsed by splitCommandParts

would produce args: ["--config", "C:Usersmecfg.json"].

In practice this is unlikely to affect the current Claude CLI use case (--stdio, --dangerously-skip-permissions, etc. are plain flags), but it is a silent correctness gap. Consider documenting that argument values containing Windows paths must be quoted, or apply a similar regex-aware treatment to the rest portion.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/acpx/src/runtime-internals/mcp-proxy.mjs
Line: 88-94

Comment:
**Unquoted Windows path with backslashes in arguments is silently corrupted**

When `splitWindowsExecutableCommand` successfully extracts the command, `rest` is passed to `splitCommandParts`. If `rest` contains an unquoted Windows path (e.g. a `--input C:\Users\data.json` argument), `splitCommandParts` is still in Unix-mode and will treat the `\` as an escape character — consuming it and yielding `C:Usersdata.json` silently.

For example:
```
C:\Program Files\Claude\claude.exe --config C:\Users\me\cfg.json
                                             ^^^^^^^^^^^^^^^^^^ parsed by splitCommandParts
```
would produce `args: ["--config", "C:Usersmecfg.json"]`.

In practice this is unlikely to affect the current Claude CLI use case (`--stdio`, `--dangerously-skip-permissions`, etc. are plain flags), but it is a silent correctness gap. Consider documenting that argument values containing Windows paths must be quoted, or apply a similar regex-aware treatment to the `rest` portion.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +61 to +71
it("parses quoted Windows executable paths without dropping backslashes", () => {
const parsed = splitCommandLine(
'"C:\\Program Files\\Claude\\claude.exe" --stdio --flag "two words"',
"win32",
);

expect(parsed).toEqual({
command: "C:\\Program Files\\Claude\\claude.exe",
args: ["--stdio", "--flag", "two words"],
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No test for double-backslash sequences inside quoted Windows paths

splitCommandParts now treats \\ inside double-quoted strings as a single \ (escape sequence), while \X (any non-special char) is kept as a literal \X. The test only exercises the literal case. A test for the escaped-backslash round-trip (i.e. the path that quoteCommandPart in mcp-agent-command.ts would actually produce — double-escaped backslashes) is missing:

it("parses double-escaped backslashes inside quoted Windows paths", () => {
  // quoteCommandPart produces "C:\\\\Program Files\\\\Claude\\\\claude.exe"
  const parsed = splitCommandLine(
    '"C:\\\\Program Files\\\\Claude\\\\claude.exe" --stdio',
    "win32",
  );
  expect(parsed).toEqual({
    command: "C:\\Program Files\\Claude\\claude.exe",
    args: ["--stdio"],
  });
});

Without this test, a regression in \\ handling could go undetected even though it would break the live code path (since the payload always stores the output of quoteCommandPart, which doubles the backslashes).

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/acpx/src/runtime-internals/mcp-agent-command.test.ts
Line: 61-71

Comment:
**No test for double-backslash sequences inside quoted Windows paths**

`splitCommandParts` now treats `\\` inside double-quoted strings as a single `\` (escape sequence), while `\X` (any non-special char) is kept as a literal `\X`. The test only exercises the literal case. A test for the escaped-backslash round-trip (i.e. the path that `quoteCommandPart` in `mcp-agent-command.ts` would actually produce — double-escaped backslashes) is missing:

```ts
it("parses double-escaped backslashes inside quoted Windows paths", () => {
  // quoteCommandPart produces "C:\\\\Program Files\\\\Claude\\\\claude.exe"
  const parsed = splitCommandLine(
    '"C:\\\\Program Files\\\\Claude\\\\claude.exe" --stdio',
    "win32",
  );
  expect(parsed).toEqual({
    command: "C:\\Program Files\\Claude\\claude.exe",
    args: ["--stdio"],
  });
});
```

Without this test, a regression in `\\` handling could go undetected even though it would break the live code path (since the payload always stores the output of `quoteCommandPart`, which doubles the backslashes).

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

extensions: acpx r: too-many-prs Auto-close: author has more than twenty active PRs. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: acpx fails to spawn Claude CLI on Windows

2 participants