Windows: a hidden Claude call cannot start when the CLI path has a space
Maintainers usually reply within 2 days
@theshloksschauhan is already working on this.
Since Oct 15, 2026.
- #432 by @theshloksschauhan — open
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 70/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- nodejs, typescript
- Domain
- desktop
Research direction
Start in src/main/hiddenClaude.ts at the winWrap lines (about 149-151), where the executable is passed unquoted to cmd.exe. Then read buildCmdCommandLine in src/main/pty.ts to see how it quotes the path. The fix should make a Windows path containing a space launch, and test/windows-seams.test.cjs is the suggested place to pin it. Confirming it needs a Windows machine, since the bug only shows there.
Written by the indexing model from the issue text.
Description
Found by reading the code rather than by hitting it in the app, so I reproduced it in
isolation instead. Details and honesty about what I did and did not verify are below.
What happens
On Windows, every hidden Claude call in the app fails to start when the resolved Claude CLI
path contains a space — which is every account whose Windows username has one, since the CLI
lands in C:\Users\<First Last>\AppData\Roaming\npm\claude.cmd.
It fails hard, with no fallback:
'C:\Users\Jane Smith\AppData\Roaming\npm' is not recognized as an internal or external command
cmd.exe is handed the executable path unquoted and splits it at the space, so the hidden
session never starts. Every caller then falls through to its own fallback path.
Steps to reproduce
In isolation, on Windows 11, with Node's child_process.spawn (which applies the same
MSDN/CRT argsToCommandLine rules node-pty applies, so the quoting behaviour is the same):
// A faithful stand-in for the npm shim: an @echo shim that hands %* to node.
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'nospace'));
const DIR = path.join(ROOT, 'AppData Roaming npm'); // <- the space
fs.mkdirSync(DIR);
fs.writeFileSync(path.join(DIR, 'claude.cmd'), '@echo off\r\n@node "%~dp0\\cli.js" %*\r\n');
fs.writeFileSync(path.join(DIR, 'cli.js'), 'console.log(JSON.stringify(process.argv.slice(2)));');
// The two flags runHiddenClaude passes (src/main/index.ts:671, :137)
const ARGS = ['--session-id', '1111...', '--setting-sources', 'user',
'--settings', '{"disableAllHooks":true}'];
spawn('cmd.exe', ['/c', path.join(DIR, 'claude.cmd'), ...ARGS]); // exit 1, not recognized
Root cause
src/main/hiddenClaude.ts imports raw node-pty and spawns directly, so it never goes
through PtyManager.spawn, and it rolls its own Windows wrapper at
src/main/hiddenClaude.ts:149-151:
const winWrap = process.platform === 'win32' && !/\.(exe|com)$/i.test(exe);
const spawnFile = winWrap ? (process.env.ComSpec || 'cmd.exe') : exe;
const spawnArgs = winWrap ? ['/c', exe, ...args] : args; // exe passed unquoted
src/main/pty.ts already solved this exact problem for the agent-terminal path, and its
comments name the failure it was written for — pty.ts:627-637:
#55: when routing through cmd.exe we must NOT passresolvedas a bare, unquoted array
element. A Program-Files path … would split on its space under cmd.exe →C:\Program is not recognizedand every Windows agent terminal dies.
PtyManager.spawn prefers decoding the npm shim and spawning node.exe with a real argv
array (resolveWindowsShimSpawn, pty.ts:505-545), and otherwise builds one fully quoted
command line — buildCmdCommandLine, pty.ts:138-150, used at pty.ts:638-640:
export function buildCmdCommandLine(resolved: string, args: string[]): string {
const quoteToken = (s: string): string => {
const escaped = s.replace(/"/g, '\\"');
return /[ \t"&|^<>()%!]/.test(s) ? `"${escaped}"` : escaped;
};
const inner = [resolved, ...args].map(quoteToken).join(' ');
return `/d /s /c "${inner}"`; // /s strips exactly the outer pair
}
It is already exported, and src/main/claudeMcpList.ts already imports it for precisely this
reason. hiddenClaude.ts just never picked it up.
Matrix
Four shapes through each route. Only the executable path having a space matters — a space in
an argument such as --add-dir "C:\Users\Jane\My Office\Pam" is already handled correctly,
because cmd's /c quote-stripping rule only fires when the first character after /c is a
quote.
| case | current ['/c', exe, ...args] |
buildCmdCommandLine |
|---|---|---|
| exe no space, args no space | exit 0, ok | exit 0, ok |
| exe WITH space, args no space | exit 1, launch failed | exit 0, ok |
exe no space, --add-dir with space |
exit 0, ok | exit 0, ok |
exe WITH space, --add-dir with space |
exit 1, launch failed | exit 0, ok |
--settings arrives as valid JSON with disableAllHooks: true in every green row, so I want to
be clear that the silent variant I first suspected — hooks quietly running because the JSON got
mangled — does not happen. This is a loud, total failure, not a quiet one.
This is CONTRIBUTING.md's "Paths with spaces are real. Several shipped bugs came from a hive
folder living under a directory with a space in its name. Quote, and test one." One seam over.
Blast radius
All seven callers of runHiddenClaude are platform-unguarded, so all seven lose their primary
path on Windows:
| Caller | What stops working |
|---|---|
src/main/index.ts:665 |
the trust layer check for Research's web answers |
src/main/memoryTidy.ts:245 |
the background memory tidy |
src/main/hireCheck.ts:60 |
the hire wizard's "does this job already exist" check |
src/main/workStyleConvert.ts:178 |
the "Suggest me" work style preview |
src/main/workStyleConvert.ts:125 |
the focus check |
src/main/workStyleConvert.ts:69 |
work style conversion |
src/main/standingCheck.ts:19 |
the standing email approval check |
Each has a fallback, which is presumably why this has gone unnoticed: the feature degrades
rather than errors.
Suggested fix
src/main/hiddenClaude.ts, one import and one line:
import { buildCmdCommandLine } from './pty';
// …
const spawnArgs = winWrap ? buildCmdCommandLine(exe, args) : args;
buildCmdCommandLine returns a string. node-pty passes string args through verbatim
(isCommandLine === true, see the comment at pty.ts:631-634), so the quoting is never
double-wrapped, and /s strips exactly the one outer quote pair.
Why this is safe here despite buildCmdCommandLine being documented as lossy
(pty.ts:130-131: "a newline can never survive"): this call site passes no multi-line argument.
Prompts reach the session over the PTY by bracketed paste, not as an argument
(hiddenClaude.ts:18-20), and the only structured argument is the single-line
JSON.stringify({ disableAllHooks: true }) from index.ts:671. So the one real cost of the
cmd.exe route does not apply, and the fix needs no follow-up work.
The strictly better fix is the one pty.ts prefers for the agent path: export
resolveWindowsShimSpawn and spawn node.exe directly, so there is no shell parser in the
chain at all and the 32767-char CreateProcess ceiling applies instead of cmd.exe's 8191. I
would suggest that as a small follow-up rather than fold it in here, since the issue above is
caused by the fallback and the shim path does not have it.
Suggested tests
test/windows-seams.test.cjsis the existing home for "a seam that already branches for
Windows keeps doing so".hiddenClaudeis not one of its five
(procKill,memory,docText,macOcr, hook commands), and
docs/designs/windows-11-installer.mditem 3 lists those same five. Adding it there keeps
the seam list honest. Assert the produced spawn starts/d /s /c "and that an exe path
with a space survives intact, using the injected-platform+ComSpecpattern that file
already uses formacOcr.test/hidden-claude-prompt-taken.test.cjs:16guards its whole body behind
const POSIX = process.platform !== 'win32'. PerCONTRIBUTING.mdthat skip needs a reason
and should not hide a real Windows bug — the argument construction in this issue is
testable on every platform and is the part worth pinning.
What I did not verify
- I did not observe this against a packaged build of the app. The reproduction is in isolation,
using Node's spawn to stand in fornode-pty's CRT quoting. - I have not measured how many of the seven callers have a fallback that covers this well. The
table lists where each call is made, not how gracefully each degrades. - Only Windows. I have not checked whether the same bare-argv shape is wrong on macOS or Linux;
winWrapis false there and the argument array goes straight to the binary, so I believe
not, but I did not test it.
Happy to open a PR for the fix and the two tests if this is worth having. I would rather not
presume it is until someone confirms the fallback behaviour I have not measured.
- Dominant language
- TypeScript
- Stars
- 273
- Forks
- 211
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 385
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from agentvivekkumar/dontbemichael
-
auto-report
Difficulty 4/5 1-2 days Newbie friendliness 35/100
agentvivekkumar/dontbemichael#680 · 2 comments ·
Maintainers usually reply within 2 days
-
auto-report
Difficulty 3/5 1-2 days Newbie friendliness 28/100
agentvivekkumar/dontbemichael#677 · 3 comments ·
Maintainers usually reply within 2 days
-
auto-report
Difficulty 4/5 3-5 days Newbie friendliness 22/100
agentvivekkumar/dontbemichael#676 · 3 comments ·
Maintainers usually reply within 2 days
-
auto-report
Difficulty 4/5 3-5 days Newbie friendliness 15/100
agentvivekkumar/dontbemichael#675 · 2 comments ·
Maintainers usually reply within 2 days
-
auto-report
Difficulty 3/5 1-2 days Newbie friendliness 20/100
agentvivekkumar/dontbemichael#674 · 2 comments ·
Maintainers usually reply within 2 days
All issues in agentvivekkumar/dontbemichael
Similar issues
-
desktop v3 windows
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
esengine/DeepSeek-Reasonix#15016 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
readest/readest#7413 · 1 comment ·
Maintainers usually reply within 1 day
-
enhancement good first issue priority: medium size: XS
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
content triage
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
eliasstravik/herdr-projects#788 ·