Windows: a hidden Claude call cannot start when the CLI path has a space

Closed Beginner friendly
#426 0 comments 0 reactions 0 assignees View on GitHub

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 pass resolved as a bare, unquoted array
element. A Program-Files path … would split on its space under cmd.exe → C:\Program is not recognized and 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

  1. test/windows-seams.test.cjs is the existing home for "a seam that already branches for
    Windows keeps doing so". hiddenClaude is not one of its five
    (procKill, memory, docText, macOcr, hook commands), and
    docs/designs/windows-11-installer.md item 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 + ComSpec pattern that file
    already uses for macOcr.
  2. test/hidden-claude-prompt-taken.test.cjs:16 guards its whole body behind
    const POSIX = process.platform !== 'win32'. Per CONTRIBUTING.md that 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 for node-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;
    winWrap is 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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from agentvivekkumar/dontbemichael

All issues in agentvivekkumar/dontbemichael

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.