Most Claude Code tooling I read about assumes macOS or Linux. I build a Windows desktop app that starts Claude Code sessions in terminals, writes commit messages with claude -p and runs hooks all day. Here's what went wrong on Windows, in the order I hit it, so you don't have to.
1. claude isn't an executable
If you installed Claude Code with npm, claude on Windows is a claude.cmd shim. spawn('claude', ...) fails, and since Node's 2024 security fix (CVE-2024-27980) spawning a .cmd without a shell throws EINVAL on purpose.
What works reliably for me is going through cmd.exe explicitly:
const args = ['/d', '/c', 'claude', '--settings', settingsFile, '--mcp-config', mcpFile] pty.spawn('cmd.exe', args, { cwd, env }) /d skips any AutoRun commands the user has in the registry, so their shell setup can't interfere.
2. Don't add /s, and don't quote the first token
cmd /c has odd rules for quotes. With /s, or when the line starts with a quote, it strips the first and last quote character of the whole command line. A settings path like "C:\Users\Jane Doe\...\settings.json" then loses one of its quotes and splits at the space.
Keep the first token (claude) unquoted and leave out /s. Then cmd leaves the quotes around your paths alone. Users with spaces in their username will thank you.
3. Everything on that command line is an injection point
Once you go through cmd.exe, &, | and friends mean something. Two rules I now follow:
- Validate anything that becomes an argument. A session ID for
--resumehas to match^[A-Za-z0-9-]{1,64}$, or the app refuses to start it. - Never pass user text as an argument. Commit diffs, PR drafts, notes typed on the phone: they all go to
claude -pthrough stdin. The prompt argument is a fixed sentence like"Follow the instructions in the input."
execFile('cmd.exe', ['/d', '/c', 'claude', '-p', 'Write the commit message as instructed in the input.', '--model', 'haiku', '--output-format', 'text'], opts) // the diff and the instructions are written to the child's stdin 4. Your Electron environment leaks into Claude
The child inherits your whole environment. If ELECTRON_RUN_AS_NODE ends up in your app's environment (for example because it was started from a tool that sets it), every Electron-based program started further down runs as plain Node instead. I delete it before spawning:
const env = { ...process.env, MY_APP_SESSION_ID: id } delete env.ELECTRON_RUN_AS_NODE Small bonus trap: Windows already has an environment variable called PROMPT (it's $P$G, the cmd prompt format). If your app reads its own PROMPT variable, it gets that.
5. Hook commands have to survive three shells
A hook command in settings.json can end up running in cmd, Git Bash or PowerShell, depending on the setup. The form that worked everywhere for me:
{ "type": "command", "command": "node \"C:\\path with spaces\\hook-forwarder.js\"" } The program (node) is unquoted, and only the path is quoted. Quoting the program too breaks in cmd for the same reason as trap 2.
6. Line endings, when Claude edits through scripts
This one isn't about spawning, but it bit me hardest. My repo is LF. Sometimes Claude Code decides to change a file with a small Python script instead of its edit tool. Python's open(path, 'w') on Windows writes CRLF, so a one-line change shows up as a diff of the entire file.
Things that help:
- A
.gitattributeswith* text=auto eol=lf, so Git normalises on commit. - A line in
CLAUDE.md: prefer the edit tool, and when a script is needed, open files withnewline=''. - Look at
git diff --statbefore every commit. A 900-line change for a typo is your hint.
Related: backslashes inside bash heredocs and non-ASCII characters in curl -d from Git Bash can arrive mangled. If the content matters, write it to a file first and send the file.
What this doesn't cover
I only use Claude Code installed through npm. The native installer may avoid traps 1 and 2 completely, and newer Claude Code versions run sessions through a background service, which changes how a terminal is attached. Check your own setup before copying any of this.
Question
If you use Claude Code on Windows: how do you run it?
A) PowerShell / Windows Terminal
B) Git Bash
C) WSL
D) inside an editor (VS Code, JetBrains)
And what's the weirdest Windows-only bug you hit with it?
(All of the code above comes from VibeDesk, an open-source Windows desktop for Claude Code, if you want to see it in context.)


