Repository navigation
Windows: plugin hooks spawn visible cmd.exe/conhost.exe windows despite windowsHide:true #27115
Description
Description
On Windows 11 Enterprise, plugin hooks spawn visible cmd.exe / conhost.exe console windows that flash on screen. This happens despite Claude Code correctly setting windowsHide: true on the spawn() call.
Each hook invocation creates a brief but visible console window flash. With multiple hooks (PreToolUse, PostToolUse, SessionStart, etc.), this produces frequent visual disruption during normal usage.
Environment
- Claude Code: 2.1.45
- OS: Windows 11 Enterprise 10.0.26200
- Node.js: v22.20.0
- Terminal: Windows Terminal
- Shell: Git Bash (MINGW64)
Steps to Reproduce
- Install Claude Code on Windows
- Install any plugin that registers hooks (e.g., hooks with
"type": "command", "command": "node script.js") - Start a session and use any tool that triggers a hook
- Observe brief cmd.exe console windows flashing on screen
Evidence from Windows Security Audit Logs
Enabled process creation auditing (Event ID 4688). The process chain for each hook invocation:
node.exe (claude) -> cmd.exe -> conhost.exe (VISIBLE WINDOW)
-> node.exe (hook script)
Key audit log entries (timestamps within same millisecond):
| Time | Parent | New Process | Note |
|---|---|---|---|
| 10:20:48.612 | node.exe | cmd.exe | CC spawns hook via shell:true |
| 10:20:48.627 | cmd.exe | conhost.exe | Console Host = visible window |
| 10:20:49.033 | node.exe | cmd.exe | Next hook fires |
| 10:20:49.040 | cmd.exe | conhost.exe | Another visible window |
Root Cause Analysis
The hook execution code (extracted from binary at offset ~122596477) does:
// Simplified from minified ow$() function
let command = hookConfig.command; // e.g. "node script.js"
let child = child_process.spawn(command, [], {
env: env,
cwd: projectDir,
shell: true, // uses cmd.exe on Windows
windowsHide: true // sets CREATE_NO_WINDOW flag
});windowsHide: true is correctly set (confirmed all 12 occurrences in binary are true). However, conhost.exe is still being spawned by cmd.exe on this Windows configuration.
Standalone test shows windowsHide works
We created a standalone test that spawns processes with identical options:
const { spawn } = require('child_process');
spawn('node -e "process.exit(0)"', [], {
shell: true,
windowsHide: true,
stdio: 'pipe'
});This test does NOT produce visible windows when run from a separate terminal. Yet the same shell: true + windowsHide: true combination from within Claude Code DOES produce visible windows. This suggests something about Claude Code's process context (e.g., console attachment state, Windows Terminal ConPTY interaction, or SEA binary characteristics) causes conhost.exe allocation despite CREATE_NO_WINDOW.
Attempted Workarounds
| Approach | Result |
|---|---|
windowsHide: true in hook scripts' own execSync/spawn calls |
Fixes secondary spawns (git commands within hooks), not the primary hook spawn |
| ComSpec=bash.exe to bypass cmd.exe | Breaks variable expansion ($VAR expanded by bash vs passed literally by cmd.exe) |
| CLAUDE_CODE_SHELL_PREFIX | Still goes through shell: true / cmd.exe, cannot bypass the issue |
| StatusLine launcher (pure Node.js, no bash wrapper) | Fixes statusLine-specific flashes but not hook flashes |
Suggested Fix
For hooks on Windows, consider one of:
-
Use
shell: falsefor hook commands that don't require shell interpretation. Most hooks are simplenode script.jsinvocations. Parse the command and spawn directly:// Instead of: spawn(command, [], { shell: true, windowsHide: true }) // Parse and use: spawn('node', ['script.js'], { shell: false, windowsHide: true })
-
Use cmd.exe /d /s /c directly with
CREATE_NO_WINDOWvia spawn options, bypassing Node.js'sshell: truehandling which may not correctly propagate the flag in all contexts. -
Investigate why
windowsHide: trueis ineffective in the Claude Code process context (SEA binary? ConPTY interaction? Console attachment state?) when it works in standalone Node.js.
Activity
kaluchi commentedon Mar 10, 2026
Any updates here?
nkabram commentedon Mar 19, 2026
I'm dealing with the same issue, any updates on this?
nkabram commentedon Mar 19, 2026
Root cause analysis + why the
CLAUDE_CODE_GIT_BASH_PATHworkaround is incompleteAfter reviewing this issue alongside #35797, #28138, and #14828, there are actually two distinct failure modes at play — and both need to be fixed on the Claude Code side, not by hook authors.
Two separate spawn paths, two separate bugs
Path A (tool execution — #14828, #28138): When Claude Code spawns
bash.exefor tool execution (Bash, Grep, Glob, etc.),windowsHide: trueis simply absent from the spawn call. Straightforward missing option.Path B (hook execution — this issue, #35797): As documented here,
windowsHide: trueis correctly set in all 12 occurrences. The problem is thatshell: truecauses Node.js to spawncmd.exeas an intermediary. TheCREATE_NO_WINDOWflag is applied to thatcmd.exespawn, butconhost.exeis allocated insidecmd.exe's own initialization — before any child-process creation flag from Node can affect it.The deeper aggravating factor is the SEA binary + ConPTY context. A vanilla Node.js process in a fresh terminal doesn't reproduce this (as shown in the standalone test above) because it doesn't carry an inherited ConPTY handle. The Claude Code SEA binary running inside Windows Terminal holds that handle, and it propagates down the spawn chain. Windows sees the descendant as attached to an existing console session via ConPTY and allocates
conhost.exeas a legacy console host for compatibility — even withCREATE_NO_WINDOWset on the parent.Hook authors only supply a command string in
settings.json. They have no control over how Claude Code spawns it — no access to spawn options, no API surface to influence the decision. Theshell: truechoice is entirely inside Claude Code's hook execution code.The correct fix: for each hook command, Claude Code should parse the command string and spawn the target executable directly with
shell: false, windowsHide: true. For the vast majority of hooks (node script.js,python hook.py, simple echo commands), no shell expansion is needed, and thecmd.exeintermediary is unnecessary. For commands that genuinely require shell expansion, explicitly callingspawn('cmd.exe', ['/d', '/s', '/c', command], { windowsHide: true })would be more reliable thanshell: true, which doesn't consistently propagate creation flags across thecmd.exeboundary in ConPTY contexts.Why
CLAUDE_CODE_GIT_BASH_PATHstill flashes intermittentlyThe GUI-subsystem wrapper approach solves the first hop — spawning a GUI binary avoids
conhost.exefor that process itself. But the wrapper then spawnsbash.exe, which spawns the hook script, which almost certainly invokes other console-subsystem binaries (node.exe,git.exe,python.exe, etc.).CREATE_NO_WINDOWdoes not propagate automatically down a full process chain. Each grandchild CUI binary evaluates console attachment independently at launch. If any process in the chain inherits the ConPTY handle from the Claude Code SEA parent, Windows may allocate a newconhost.exefor that grandchild even if its immediate parent was a GUI-subsystem process. The flash you see with the workaround in place is a grandchild's window, notbash.exe's.Fully suppressing this at the wrapper level would require explicitly closing or detaching all inherited console handles via Win32 API calls (
FreeConsole(), explicitHANDLEmanagement) before spawning bash — well beyond a simple wrapper binary. This is exactly why the fix belongs in Claude Code: it controls the root of the spawn tree and can suppress handle inheritance at the source, which would suppress the problem for the entire subtree.pauljones0 commentedon Apr 13, 2026
This issue makes me mad. I'm paying good money at Max 5x and my claude code crashes my laptop because it's spawning 150+ windows. This is digusting. If you want to see what vibe coded trash looks like, look no further than claude code. I cannot test my application because BOOM. Has this issue been fixed? No. Did my code use to work? yes. Why oh why
ACU-Leigh commentedon May 15, 2026
Really need a fix for this!!
github-actions commentedon Jun 13, 2026
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
github-actions commentedon Aug 16, 2026
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.