Skip to content

fix: Embedded Claude panel: start on Windows, locate the CLI without a shell - #21

Open
lt3-me wants to merge 2 commits into
HelpFreedom:mainfrom
lt3-me:windows-claude-panel
Open

fix: Embedded Claude panel: start on Windows, locate the CLI without a shell#21
lt3-me wants to merge 2 commits into
HelpFreedom:mainfrom
lt3-me:windows-claude-panel

Conversation

@lt3-me

@lt3-me lt3-me commented Sep 8, 2026

Copy link
Copy Markdown

Problem

On Windows the Claude panel fails before it ever reaches the CLI:

Error: File not found:

Installing or reinstalling claude does not help, because the error is about
/bin/bash, not about claude. electron/claude.ts located the CLI with
/bin/sh -c "command -v claude" and spawned it inside a /bin/bash -c <watchdog> wrapper;
neither shell exists on Windows, so ConPTY reports the wrapper as missing.

What changes

CLI lookup moves from the shell into node (resolveCommand). A bare name
is walked along the session's PATH — the one claude-env.json may extend,
which the old lookup ignored — with PATHEXT on Windows. That finds every way
the CLI gets installed: npm's claude.cmd shim, the native installer's
claude.exe, bun/pnpm/volta shims. Plain CreateProcess would only find an
.exe, and where claude lists npm's extensionless sh-script shim first,
which ConPTY cannot start. After PATH the lookup checks ~/.local/bin, the
native installer's directory on every platform, for an editor launched from
a desktop entry whose PATH has not caught up with the install. Names with a
separator are taken as paths, ~ is expanded.

A missing CLI is reported by name, together with the path of
claude-env.json, instead of surfacing as the pty's "File not found".

Windows spawns the launcher directly, without the watchdog. The console
takes the watchdog's role there: the pseudoconsole belongs to the Electron
process, so a hard death takes the console host down and every attached
process gets CTRL_CLOSE_EVENT. Closing the panel uses node-pty's ConPTY
kill, which terminates the console's whole process list, claude and its MCP
children included. There are no process groups to signal on Windows.

Env overrides merge case-insensitively on Windows, so a PATH in
claude-env.json replaces the inherited Path rather than sitting next to
it as a second spelling of the same variable.

The Linux path is untouched: same bash wrapper, same SIGHUP-then-SIGKILL to
the process group. CLAUDE.md gains a paragraph on all of the above.

Verification

Windows 11, Electron 42, claude 2.1.265 installed from npm, driven over CDP:

  • claudeOpenok: true; process tree is cmd.exe → claude.exe, and the
    --append-system-prompt argument reaches claude.exe intact, including the
    embedded double quotes and the non-ASCII arrows.
  • claudeClose → no claude/cmd/node process left.
  • taskkill /F on the Electron main process → claude.exe and cmd.exe gone
    within seconds, no Electron processes left either.
  • KADR_CLAUDE_CMD=definitely-missing-cli → the new error text with the
    command name and the config path.
  • npm run typecheck clean.

Not exercised here: the Linux path. The only change there is the lookup
(command -v → PATH walk with identical semantics for a non-interactive
shell), and the suites that override command: "bash" — e2e22, e2e23,
e2e24, e2e31, e2e34 — cover it.

A run on Linux before merging would be welcome!!!

Known limits, deliberately out of scope

  • A .cmd launcher runs through cmd.exe, whose parser rewrites %VAR%, ^
    and unquoted & | < >. The built-in arguments avoid those; a user args
    override on Windows has to as well, or command can point straight at the
    .exe the shim wraps. Documented in the code and in CLAUDE.md.
  • sweepStaleSessions still does nothing on Windows. The console lifetime
    covers the claude tree; the ffmpeg export/proxy workers it also sweeps on
    Linux are a separate task.
  • macOS apps launched from the Dock get a minimal PATH without
    /opt/homebrew/bin; the resolver does not special-case it, since I could
    not test there.

🤖 Generated with Claude Code

lt3-me and others added 2 commits September 9, 2026 02:58
The panel never got as far as the CLI on Windows. `spawnSession` located
`claude` with `/bin/sh -c "command -v claude"` and started it inside a
`/bin/bash -c <watchdog>` wrapper — neither shell exists there, so ConPTY
answered «File not found» about bash, and the panel showed «не удалось
запустить claude: Error: File not found:» whatever the state of the CLI.

### Locating the CLI

`resolveCommand` replaces the shell lookup. A non-interactive sh reads no
rc file, so its PATH was ours anyway; the walk is now done in node, along
the SESSION's PATH (the one claude-env.json may extend — the header comment
promised a custom PATH would work, and it did not reach the lookup), with
PATHEXT on Windows. That is what finds every installer's launcher: npm's
`claude.cmd` shim, the native installer's `claude.exe`, bun/pnpm/volta
shims. CreateProcess on its own tries `.exe` and nothing else, and
`where claude` lists npm's extensionless sh-script shim first — a file
ConPTY cannot start. After PATH comes `~/.local/bin`, where the native
installer puts the binary on every platform, for an editor launched from a
desktop entry whose PATH has not caught up. A name with a separator is a
path; `~` is expanded, as sh used to.

A CLI that is not found is now reported by name, with the path of
claude-env.json, instead of surfacing as the pty's error.

### Starting and stopping it

On Windows the launcher is spawned directly, without the watchdog. The
console is the watchdog there: the pseudoconsole is a handle of the
Electron process, so a hard death takes the console host with it and every
attached process receives CTRL_CLOSE_EVENT — verified with `taskkill /F` on
the main process, claude.exe and its cmd.exe were gone within seconds.
Closing the panel calls node-pty's ConPTY kill, which terminates the
console's whole process list (claude and its MCP children); there are no
process groups to signal. The Linux path — bash wrapper, SIGHUP then
SIGKILL to the group — is unchanged.

Environment overrides from claude-env.json are merged case-insensitively
on Windows: a `PATH` next to the inherited `Path` would hand CreateProcess
two spellings of one variable, and which one the child sees is not defined.

One caveat, recorded in the code and in CLAUDE.md: a `.cmd` launcher is
run by CreateProcess through cmd.exe, whose parser rewrites `%VAR%`, `^`
and unquoted `& | < >`. The built-in arguments carry none of those —
verified that the quotes and the non-ASCII arrows in the system hint reach
claude.exe intact — and a user `args` override on Windows has to stay
equally plain, or point `command` at the `.exe` the shim wraps.

### Verification

On Windows 11, Electron 42, claude 2.1.265 from npm, over CDP:
`claudeOpen` returns ok and the tree is cmd.exe → claude.exe with the
arguments whole; `claudeClose` leaves no process behind; a hard kill of
the Electron main process leaves no process behind; a bogus command name
returns the new error text. `npm run typecheck` is clean. The Linux path
was not exercised on this machine — the only change there is the lookup,
and the e2e suites that set `command: "bash"` (e2e22–e2e34) cover it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lpFreedom#7 in view

PR HelpFreedom#7 (simplaerai-sv, «Windows support for the embedded Claude session»)
solves the same failure on the v0.3 base. This compares the two and takes
the one idea from it worth taking.

### Taken: the explicit cmd.exe route

`winLaunch` now starts a `.cmd`/`.bat` launcher — npm's `claude.cmd` shim —
through `%ComSpec%` itself instead of letting CreateProcess supply the
interpreter. Two things come with that. `/d` skips the registry AutoRun
(this machine has clink there; a conda hook is the other usual tenant),
which otherwise runs inside the panel's cmd.exe and can print into it.
And the quoting becomes ours: `/s` plus an outer pair of quotes around the
whole line, which keeps a launcher path with a space in it whole.

That last part is where PR HelpFreedom#7's version breaks. It spawns
`cmd.exe /c "<shim>" args...` bare, and cmd's parser strips the first and
the last quote of its command when the first character is a quote — so
`"C:\Users\Jane Doe\AppData\Roaming\npm\claude.cmd" --mcp-config ...`
becomes `C:\Users\Jane Doe\...` and cmd stops at `C:\Users\Jane`. A user
name with a space is not exotic. Verified with a shim in a directory with
a space: bare `/c` fails as described, the outer-quoted form and the
implicit CreateProcess form both deliver every argument intact, inner
quotes and non-ASCII included.

### Not taken: `where.exe`

PR HelpFreedom#7 resolves the CLI with `where.exe` and prefers an `.exe`/`.cmd`/`.bat`
hit, falling back to the first line — which is npm's extensionless sh
shim, the file ConPTY cannot start. The PATH walk here uses PATHEXT, so
that shim is never a candidate; it searches the SESSION's PATH, which
claude-env.json may extend; and it checks `~/.local/bin`, the native
installer's directory, for an editor launched from a desktop entry whose
PATH has not caught up. It also spawns no helper process.

Session teardown is the same in both: `pty.kill()` on Windows, node-pty's
ConPTY kill enumerating the console's process list.

### Verification, on the real UI this time

Windows 11, claude 2.1.265 from npm, the panel driven by hand:

- open → one `cmd.exe /d /s /c "…claude.CMD …"` and one claude.exe;
- close → nothing left; close-and-reopen in quick succession → exactly
  one tree; quit the editor with the panel open → nothing left;
- `/mcp` inside the panel: kadr connected, 13 tools; the same 13 over a
  stdio client spawning mcp-bridge.cjs like claude does; kadr_state and
  kadr_eval answer; /eval refuses a request without the token and one
  carrying an Origin (403 both);
- close/reopen regenerates kadr-mcp.json with a fresh bridge port;
- the trust dialog that appears with no project open is the CLI's own:
  the panel starts in the home directory then, and the CLI does not
  persist trust for it even when run from a plain terminal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@lt3-me

lt3-me commented Sep 9, 2026

Copy link
Copy Markdown
Author

How this differs from #7

#7 (simplaerai-sv) fixes the same failure on the v0.3 base and was rebased onto
v0.5.1 without a re-run on Windows. This PR was written against v0.5.1 and every
claim below was checked on Windows 11 with claude 2.1.265 from npm, driving the
real panel by hand. The second commit here (a723a3b) is a direct response to #7:
it takes the one idea worth taking and explains what is not taken.

Locating the CLI

#7 this PR
Method where.exe, prefer an .exe/.cmd/.bat hit PATH walk in node with PATHEXT
Fallback when nothing matches first where line — npm's extensionless sh shim, which ConPTY cannot start a clear error naming the command and claude-env.json
PATH searched the process's the session's — the one claude-env.json may extend, which the comment in the file has always promised would work
Installer directories none ~/.local/bin (the native installer, every platform)
~ in command no expanded
Extra process one where.exe per open none

Every way the CLI gets installed is found the same way: npm's claude.cmd shim,
the native installer's claude.exe, bun/pnpm/volta shims.

Starting a .cmd launcher

Both PRs route npm's .cmd shim through cmd.exe. #7 spawns
cmd.exe /c "<shim>" args... bare. cmd strips the first and the last quote of
its command when the first character is a quote, so for a user whose profile
path contains a space —

"C:\Users\Jane Doe\AppData\Roaming\npm\claude.cmd" --mcp-config ... "You are ..."

— cmd sees C:\Users\Jane Doe\... and stops at C:\Users\Jane. This PR spawns
%ComSpec% /d /s /c "<whole line>": /s plus the outer quotes keep the path in
one piece, /d keeps the registry AutoRun (clink, conda hooks) out of the
panel's cmd.exe. Verified with a shim in a directory with a space: the bare
/c form fails as described, the outer-quoted form delivers every argument
intact, inner quotes and non-ASCII included.

Environment

claude-env.json overrides are merged case-insensitively on Windows, so a
PATH there replaces the inherited Path instead of becoming a second spelling
of the same variable (which one CreateProcess hands the child is not defined).
#7 does not touch this.

Teardown

Same in both: pty.kill() on Windows — node-pty's ConPTY kill terminates the
console's whole process list — and the POSIX path unchanged. This PR also
verified the hard case: taskkill /F on the Electron main process leaves no
claude.exe behind, because the pseudoconsole dies with its owner and every
attached process receives CTRL_CLOSE_EVENT.

#7's own checklist, re-run here

  • the panel opens; /mcp inside it shows kadr connected with all 13 tools
    (there were six at v0.3);
  • the same 13 tools over a stdio client that spawns mcp-bridge.cjs exactly
    as claude does; kadr_state and kadr_eval answer; /eval refuses a
    request without the token and one carrying an Origin (403 both);
  • close → no process left; close and reopen in quick succession → exactly one
    tree; quit the editor with the panel open → no process left;
  • close/reopen regenerates kadr-mcp.json with a fresh bridge port;
  • the session cwd follows the saved project folder (unchanged code).

🤖 Generated with Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant