Summary
On Windows, calling start_process with a command that is itself a shell invocation (e.g. powershell, powershell -NoProfile, cmd, bash) returns a PID successfully but the stdio channel is unusable. interact_with_process calls return ok but produce no effect, no prompt indicator is detected, and a visible orphan console window appears on the desktop.
Root cause is at the Win32 console layer: the inner shell calls AllocConsole() and switches its REPL to that console buffer, bypassing the inherited stdio pipes that DC opened.
Repro
- Windows 11, Node 24,
@wonderwhy-er/desktop-commander 0.2.38
- DC config has
"defaultShell": "powershell.exe"
- From a Claude Desktop session, call:
start_process({ command: "powershell -NoProfile -NoLogo", timeout_ms: 5000 })
- Observe response: PID returned, but the
(detected: ">") prompt indicator is missing
- Call
interact_with_process({ pid: <pid>, input: "Get-Date" }) — returns ok but no output
- A visible PowerShell window appears on the desktop, orphaned (its parent process exits)
Expected
- Prompt indicator detected
interact_with_process sends input to the REPL and reads output back
- No visible window
- No orphan
Mechanism
In dist/terminal-manager.js (lines 30-36), getShellSpawnArgs for PowerShell hardcodes the -Command wrapping:
if (shellName === 'powershell' || shellName === 'powershell.exe') {
return {
executable: shellPath,
args: ['-Command', command],
useShellOption: false
};
}
When the user's command is itself a shell invocation, the resulting spawn is:
spawn("powershell.exe", ["-Command", "powershell -NoProfile -NoLogo"], {
stdio: ['pipe','pipe','pipe'],
windowsHide: true
})
Process tree at runtime:
- Outer powershell (DC's direct child): no console (
windowsHide: true), stdio pipes inherited from DC. Runs the -Command payload and blocks on WaitForExit of its child.
- Inner powershell (
-NoProfile -NoLogo): starts interactively, detects no inherited console, calls AllocConsole() -> gets its own visible console window.
- Inner's REPL uses
[Console]::ReadLine(), which reads from the new console buffer, not from the inherited stdin pipe.
- DC tracks the outer's PID.
interact_with_process writes bytes to the outer's stdin pipe. The outer is blocked on WaitForExit and does no forwarding. Bytes never reach the inner's REPL.
- Inner's prompts (
PS C:\>) write to the new console, not to the inherited stdout pipe. DC's prompt detection reads from the pipe and sees nothing.
Note: windowsHide: true does not help — it only suppresses the immediate child's window. Once the inner calls AllocConsole(), the window appears anyway. Removing windowsHide doesn't fix it either; the console split happens regardless. Win32 console redirection is sticky once AllocConsole runs.
This affects every nested-shell case on Windows: powershell, pwsh, cmd, bash (under WSL/Git Bash), sh, zsh, fish.
Proposed fix
When the user's command is itself a shell invocation, skip the -Command wrapping and spawn that shell directly so it becomes DC's direct child. With pipes as stdio, the shell uses the pipes and does not call AllocConsole.
Patch in getShellSpawnArgs:
function getShellSpawnArgs(shellPath, command) {
const shellName = path.basename(shellPath).toLowerCase();
// If the user's command is itself a shell invocation, spawn it directly
// so it inherits stdio pipes and avoids AllocConsole on Windows.
const SHELL_RE = /^(powershell|pwsh|cmd|bash|sh|zsh|fish)(\.exe)?(\s|$)/i;
const trimmed = command.trim();
if (SHELL_RE.test(trimmed)) {
const parts = trimmed.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
return {
executable: parts[0],
args: parts.slice(1).map(s => s.replace(/^"|"$/g, '')),
useShellOption: false
};
}
// ... existing logic ...
}
~15 lines. execute_command (one-shot) is unaffected because callers don't typically pass a bare shell name to it.
Workaround for users until this lands
- Use
execute_command for one-shot commands
- Use
start_process only with non-shell REPLs that respect inherited stdio: python -i, node, sqlite3 mydb.db. Those work because the single-process REPL inherits pipes correctly without the nested-shell AllocConsole problem.
- On Windows, do not use
start_process with powershell, pwsh, cmd, or bash until fixed.
Diagnosis credit
Independent verification by a second AI assistant (Windsurf Cascade) corrected an initial misdiagnosis (timing race) to the actual mechanism (AllocConsole stdio split). Worth noting in case anyone tries to test the fix by "keeping the outer alive longer" — that won't validate anything because the console split happens at inner spawn time regardless of outer lifetime.
Summary
On Windows, calling
start_processwith a command that is itself a shell invocation (e.g.powershell,powershell -NoProfile,cmd,bash) returns a PID successfully but the stdio channel is unusable.interact_with_processcalls return ok but produce no effect, no prompt indicator is detected, and a visible orphan console window appears on the desktop.Root cause is at the Win32 console layer: the inner shell calls
AllocConsole()and switches its REPL to that console buffer, bypassing the inherited stdio pipes that DC opened.Repro
@wonderwhy-er/desktop-commander0.2.38"defaultShell": "powershell.exe"start_process({ command: "powershell -NoProfile -NoLogo", timeout_ms: 5000 })(detected: ">")prompt indicator is missinginteract_with_process({ pid: <pid>, input: "Get-Date" })— returns ok but no outputExpected
interact_with_processsends input to the REPL and reads output backMechanism
In
dist/terminal-manager.js(lines 30-36),getShellSpawnArgsfor PowerShell hardcodes the-Commandwrapping:When the user's
commandis itself a shell invocation, the resulting spawn is:Process tree at runtime:
windowsHide: true), stdio pipes inherited from DC. Runs the-Commandpayload and blocks onWaitForExitof its child.-NoProfile -NoLogo): starts interactively, detects no inherited console, callsAllocConsole()-> gets its own visible console window.[Console]::ReadLine(), which reads from the new console buffer, not from the inherited stdin pipe.interact_with_processwrites bytes to the outer's stdin pipe. The outer is blocked onWaitForExitand does no forwarding. Bytes never reach the inner's REPL.PS C:\>) write to the new console, not to the inherited stdout pipe. DC's prompt detection reads from the pipe and sees nothing.Note:
windowsHide: truedoes not help — it only suppresses the immediate child's window. Once the inner callsAllocConsole(), the window appears anyway. RemovingwindowsHidedoesn't fix it either; the console split happens regardless. Win32 console redirection is sticky onceAllocConsoleruns.This affects every nested-shell case on Windows:
powershell,pwsh,cmd,bash(under WSL/Git Bash),sh,zsh,fish.Proposed fix
When the user's command is itself a shell invocation, skip the
-Commandwrapping and spawn that shell directly so it becomes DC's direct child. With pipes as stdio, the shell uses the pipes and does not callAllocConsole.Patch in
getShellSpawnArgs:~15 lines.
execute_command(one-shot) is unaffected because callers don't typically pass a bare shell name to it.Workaround for users until this lands
execute_commandfor one-shot commandsstart_processonly with non-shell REPLs that respect inherited stdio:python -i,node,sqlite3 mydb.db. Those work because the single-process REPL inherits pipes correctly without the nested-shellAllocConsoleproblem.start_processwithpowershell,pwsh,cmd, orbashuntil fixed.Diagnosis credit
Independent verification by a second AI assistant (Windsurf Cascade) corrected an initial misdiagnosis (timing race) to the actual mechanism (
AllocConsolestdio split). Worth noting in case anyone tries to test the fix by "keeping the outer alive longer" — that won't validate anything because the console split happens at inner spawn time regardless of outer lifetime.