quoteShellArg wraps paths in POSIX single quotes. cmd.exe does not strip them, so the program is handed the quotes verbatim and fails as soon as the path contains a space.
Measured on Windows 11, node 24.14.0, with a path containing a space:
| shell |
single quotes |
double quotes |
cmd.exe |
fails |
works |
| PowerShell |
works |
works |
| bash (Git Bash) |
works |
works |
The failure:
node 'C:\...\q dir\e.js'
node:internal/modules/cjs/loader:1459
Error: Cannot find module 'C:\...\'C:\...\q dir\e.js''
Where it bites
The quoted string is not executed by us. It is written into generated agent context and into CLI hints, for a human or an agent to paste into whatever shell they happen to have. On Windows that is often cmd.exe.
packages/cli/src/commands/update-ai.ts:97 — the dev-checkout rung of inferFastCliCommand
packages/cli/src/commands/update-ai.ts:132 — the sibling n8n-manager path
packages/cli/src/commands/promote.ts:1077 — a workflow credential-required hint
packages/cli/src/core/services/sync-manager.ts:398,427 — n8nac push <path> hints
packages/vscode-extension/src/extension.ts:2429,2444 — the extension's own command override
Four identical copies
The same three-line function is defined four times, at update-ai.ts:64, promote.ts:1238, sync-manager.ts:450 and extension.ts:2447. All four are byte-identical and all four are wrong the same way. That duplication is why the blast radius is what it is.
Fix
Double quotes on Windows, single quotes elsewhere. Double quotes are understood by cmd.exe, PowerShell and bash alike; single quotes remain right on POSIX because they suppress every expansion.
Consolidate the three copies inside packages/cli into one helper. The extension keeps its own, because a three-line pure function does not justify widening the n8nac public type surface across a package boundary.
Known ceiling worth writing down rather than pretending away: on Windows a path containing $ or a backtick still expands under PowerShell and bash. Both characters are legal in Windows filenames, and no escaping satisfies all three shells at once.
Not affected
The local-install rung added in #657 emits a relative path with no spaces, so it never reaches this helper.
quoteShellArgwraps paths in POSIX single quotes.cmd.exedoes not strip them, so the program is handed the quotes verbatim and fails as soon as the path contains a space.Measured on Windows 11, node 24.14.0, with a path containing a space:
cmd.exeThe failure:
Where it bites
The quoted string is not executed by us. It is written into generated agent context and into CLI hints, for a human or an agent to paste into whatever shell they happen to have. On Windows that is often
cmd.exe.packages/cli/src/commands/update-ai.ts:97— the dev-checkout rung ofinferFastCliCommandpackages/cli/src/commands/update-ai.ts:132— the sibling n8n-manager pathpackages/cli/src/commands/promote.ts:1077— aworkflow credential-requiredhintpackages/cli/src/core/services/sync-manager.ts:398,427—n8nac push <path>hintspackages/vscode-extension/src/extension.ts:2429,2444— the extension's own command overrideFour identical copies
The same three-line function is defined four times, at
update-ai.ts:64,promote.ts:1238,sync-manager.ts:450andextension.ts:2447. All four are byte-identical and all four are wrong the same way. That duplication is why the blast radius is what it is.Fix
Double quotes on Windows, single quotes elsewhere. Double quotes are understood by
cmd.exe, PowerShell and bash alike; single quotes remain right on POSIX because they suppress every expansion.Consolidate the three copies inside
packages/cliinto one helper. The extension keeps its own, because a three-line pure function does not justify widening then8nacpublic type surface across a package boundary.Known ceiling worth writing down rather than pretending away: on Windows a path containing
$or a backtick still expands under PowerShell and bash. Both characters are legal in Windows filenames, and no escaping satisfies all three shells at once.Not affected
The local-install rung added in #657 emits a relative path with no spaces, so it never reaches this helper.