Skip to content

Windows: plugin hooks spawn visible cmd.exe/conhost.exe windows despite windowsHide:true #27115

Closed as not planned

Description

@ls-edwin-bekaert

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

  1. Install Claude Code on Windows
  2. Install any plugin that registers hooks (e.g., hooks with "type": "command", "command": "node script.js")
  3. Start a session and use any tool that triggers a hook
  4. 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:

  1. Use shell: false for hook commands that don't require shell interpretation. Most hooks are simple node script.js invocations. 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 })
  2. Use cmd.exe /d /s /c directly with CREATE_NO_WINDOW via spawn options, bypassing Node.js's shell: true handling which may not correctly propagate the flag in all contexts.

  3. Investigate why windowsHide: true is ineffective in the Claude Code process context (SEA binary? ConPTY interaction? Console attachment state?) when it works in standalone Node.js.

Activity

  1. kaluchi commented on Mar 10, 2026

    @kaluchi

    Any updates here?

  2. nkabram commented on Mar 19, 2026

    @nkabram

    I'm dealing with the same issue, any updates on this?

  3. nkabram commented on Mar 19, 2026

    @nkabram

    Root cause analysis + why the CLAUDE_CODE_GIT_BASH_PATH workaround is incomplete

    After 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.exe for tool execution (Bash, Grep, Glob, etc.), windowsHide: true is simply absent from the spawn call. Straightforward missing option.

    Path B (hook execution — this issue, #35797): As documented here, windowsHide: true is correctly set in all 12 occurrences. The problem is that shell: true causes Node.js to spawn cmd.exe as an intermediary. The CREATE_NO_WINDOW flag is applied to that cmd.exe spawn, but conhost.exe is allocated inside cmd.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.exe as a legacy console host for compatibility — even with CREATE_NO_WINDOW set 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. The shell: true choice 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 the cmd.exe intermediary is unnecessary. For commands that genuinely require shell expansion, explicitly calling spawn('cmd.exe', ['/d', '/s', '/c', command], { windowsHide: true }) would be more reliable than shell: true, which doesn't consistently propagate creation flags across the cmd.exe boundary in ConPTY contexts.


    Why CLAUDE_CODE_GIT_BASH_PATH still flashes intermittently

    The GUI-subsystem wrapper approach solves the first hop — spawning a GUI binary avoids conhost.exe for that process itself. But the wrapper then spawns bash.exe, which spawns the hook script, which almost certainly invokes other console-subsystem binaries (node.exe, git.exe, python.exe, etc.).

    CREATE_NO_WINDOW does 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 new conhost.exe for 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, not bash.exe's.

    Fully suppressing this at the wrapper level would require explicitly closing or detaching all inherited console handles via Win32 API calls (FreeConsole(), explicit HANDLE management) 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.

  4. pauljones0 commented on Apr 13, 2026

    @pauljones0

    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

  5. ACU-Leigh commented on May 15, 2026

    @ACU-Leigh

    Really need a fix for this!!

  6. github-actions commented on Jun 13, 2026

    @github-actions

    Closing for now — inactive for too long. Please open a new issue if this is still relevant.

  7. github-actions commented on Aug 16, 2026

    @github-actions

    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.

  8. locked as resolved and limited conversation to collaborators on Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    staleIssue is inactive

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions