Windows: plugin hooks spawn visible cmd.exe/conhost.exe windows despite windowsHide:true
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:
``js``
// 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.
This issue has 6 comments on GitHub. Read the full discussion on GitHub ↗