Windows: Hook commands cause brief console window flash

Status Closed — not planned
Maintainer reply None cached
Activity 11 comments · opened Jan 18, 2026 · closed Feb 28, 2026

Problem

On Windows, hook commands (both SessionStart and statusLine) cause a brief console window flash when they spawn Node.js or other console applications.

This happens because the child process is spawned without the windowsHide option.

Expected Behavior

Hook commands should run invisibly without any visible window flash.

Suggested Fix

When spawning hook commands on Windows, use the windowsHide: true option in Node.js spawn/exec:

const { spawn } = require('child_process');

spawn(command, args, {
  windowsHide: true,  // Hides the console window on Windows, ignored on other platforms
  // ... other options
});

This option is cross-platform safe - it's ignored on macOS/Linux.

Current Workarounds

  1. VBS wrapper (for hooks that don't need stdout):

``vbs
Set WshShell = CreateObject("WScript.Shell")
WshShell.Run "node script.js", 0, False
``

  1. Using .cmd files - reduces but doesn't eliminate the flash

Neither workaround fully solves the problem for statusLine which requires stdout capture.

Environment

  • OS: Windows 11
  • Claude Code version: v2.1.12
  • Shell: PowerShell

Related

This affects any Windows user with custom hooks that run console applications (Node.js, Python, etc.).

View original on GitHub ↗

11 Comments

github-actions[bot] · 7 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/14828
  2. https://github.com/anthropics/claude-code/issues/15572
  3. https://github.com/anthropics/claude-code/issues/17230

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

gsa9 · 7 months ago

Update: Findings from Testing

After testing various approaches, discovered that different hook types behave differently:

Key Finding

| Hook Type | Spawn Behavior | node -e Flash? |
|-----------|---------------|------------------|
| statusLine | stdin/stdout piped | No flash ✓ |
| SessionStart | Detached spawn | Flash on Windows ✗ |

Working Workarounds

For statusLine - node -e works without flash:

"command": "node -e \"require(require('os').homedir() + '/.claude/hooks/statusline.js')\""

For SessionStart on Windows - requires mshta trick:

"command": "mshta vbscript:Execute(\"CreateObject(\"\"Wscript.Shell\"\").Run \"\"node \"\"%USERPROFILE%\.claude\hooks\gsd-check-update.js\"\"\"\", 0:close\")"

Request

Adding windowsHide: true to the spawn options for SessionStart (and other detached hooks) would:

  1. Eliminate the need for Windows-specific workarounds
  2. Allow the same cross-platform command to work everywhere
  3. Simplify hook configuration for Windows users

The fact that statusLine already doesn't flash suggests the infrastructure supports it - it may just need to be applied to SessionStart and similar hooks.

Related

Working solution documented in glittercowboy/get-shit-done#144

topemalheiro · 7 months ago

Anthropic please fix this, it's really annoying and we want to use claude-mem.

Airflyctl · 7 months ago

Not only do the windows flash, but the main CC window also sometimes crashes when using claude-mem.

jthweny · 7 months ago

I've found a bulletproof solution that completely eliminates the console flash without modifying Claude Code's source. Full research report with technical details: https://claude.ai/public/artifacts/0cd072dd-5e18-4362-9a5b-a184d9dde894

The Root Cause

When Claude Code spawns hooks with child_process.spawn(..., {shell: true}), Node.js invokes cmd.exe via the COMSPEC environment variable. The console window flash happens at CreateProcess time—before any JavaScript executes—which is why windowsHide: true doesn't help.

The Solution

  1. Create a GUI subsystem wrapper (no automatic console allocation)
  2. Name it cmd.exe and place it in a user directory (e.g., C:\Users\<user>\.claude\cmd.exe)
  3. Set COMSPEC to point to this wrapper before launching Claude Code
  4. The wrapper spawns the real cmd.exe with CREATE_NO_WINDOW flag and forwards stdio

⚠️ Critical Discovery: The Wrapper MUST Be Named cmd.exe

Node.js uses this regex to detect cmd.exe: /^(?:.*\\)?cmd(?:\.exe)?$/i

If you name it hiddencmd.exe, Node.js will:

  • Use -c instead of /d /s /c (breaking cmd.exe syntax)
  • Apply wrong quoting/escaping rules
  • Fail silently or produce incorrect behavior

Implementation

hiddencmd.cpp (compile as GUI subsystem):

#define UNICODE
#define _UNICODE
#include <windows.h>

int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
    // Get real cmd.exe path
    WCHAR realCmd[MAX_PATH];
    GetSystemDirectoryW(realCmd, MAX_PATH);
    wcscat_s(realCmd, MAX_PATH, L"\\cmd.exe");

    // Preserve original command line exactly
    LPWSTR cmdLine = GetCommandLineW();

    // Setup stdio inheritance
    STARTUPINFOW si = { sizeof(si) };
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
    si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    si.hStdError = GetStdHandle(STD_ERROR_HANDLE);

    PROCESS_INFORMATION pi;
    if (!CreateProcessW(realCmd, cmdLine, NULL, NULL, TRUE,
                        CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
        return GetLastError();
    }

    WaitForSingleObject(pi.hProcess, INFINITE);
    DWORD exitCode;
    GetExitCodeProcess(pi.hProcess, &exitCode);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return exitCode;
}

Build (MSVC):

cl /nologo /O2 /W4 /DUNICODE /D_UNICODE hiddencmd.cpp /link /SUBSYSTEM:WINDOWS /OUT:C:\Users\%USERNAME%\.claude\cmd.exe

Build (MinGW-w64):

g++ -O2 -municode -Wl,-subsystem,windows -o C:\Users\%USERNAME%\.claude\cmd.exe hiddencmd.cpp

Launcher integration (set COMSPEC before launching Claude Code):

$env:COMSPEC = "C:\Users\$env:USERNAME\.claude\cmd.exe"
& "C:\path\to\claude.exe" $args

Why This Works

  • Completely eliminates flash (not just reduces)
  • No admin privileges required
  • Survives Claude Code updates (lives in your launcher)
  • Scoped to Claude Code only (other apps unaffected)
  • Preserves stdout/stderr (hooks can output normally)
  • Preserves exit codes

What Doesn't Work

| Approach | Why It Fails |
|----------|--------------|
| windowsHide: true | Window created before option is read |
| NODE_OPTIONS preload | Same—too late |
| nodew.exe | Can't write to stdout |
| Patching cli.js | Reverted on updates, still flashes |
| Job Objects | Can't enforce CREATE_NO_WINDOW on children |
| PATH-based shim | Windows resolves cmd.exe from System32 first |

topemalheiro · 7 months ago
I've found a bulletproof solution that completely eliminates the console flash without modifying Claude Code's source. Full research report with technical details: https://claude.ai/public/artifacts/0cd072dd-5e18-4362-9a5b-a184d9dde894 ### The Root Cause When Claude Code spawns hooks with child_process.spawn(..., {shell: true}), Node.js invokes cmd.exe via the COMSPEC environment variable. The console window flash happens at CreateProcess time—before any JavaScript executes—which is why windowsHide: true doesn't help. ### The Solution 1. Create a GUI subsystem wrapper (no automatic console allocation) 2. Name it cmd.exe and place it in a user directory (e.g., C:\Users\<user>\.claude\cmd.exe) 3. Set COMSPEC to point to this wrapper before launching Claude Code 4. The wrapper spawns the real cmd.exe with CREATE_NO_WINDOW flag and forwards stdio ### ⚠️ Critical Discovery: The Wrapper MUST Be Named cmd.exe Node.js uses this regex to detect cmd.exe: /^(?:.*\\)?cmd(?:\.exe)?$/i If you name it hiddencmd.exe, Node.js will: Use -c instead of /d /s /c (breaking cmd.exe syntax) Apply wrong quoting/escaping rules Fail silently or produce incorrect behavior ### Implementation hiddencmd.cpp (compile as GUI subsystem): #define UNICODE #define _UNICODE #include <windows.h> int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { // Get real cmd.exe path WCHAR realCmd[MAX_PATH]; GetSystemDirectoryW(realCmd, MAX_PATH); wcscat_s(realCmd, MAX_PATH, L"\\cmd.exe"); // Preserve original command line exactly LPWSTR cmdLine = GetCommandLineW(); // Setup stdio inheritance STARTUPINFOW si = { sizeof(si) }; si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); si.hStdError = GetStdHandle(STD_ERROR_HANDLE); PROCESS_INFORMATION pi; if (!CreateProcessW(realCmd, cmdLine, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { return GetLastError(); } WaitForSingleObject(pi.hProcess, INFINITE); DWORD exitCode; GetExitCodeProcess(pi.hProcess, &exitCode); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return exitCode; } Build (MSVC): cl /nologo /O2 /W4 /DUNICODE /D_UNICODE hiddencmd.cpp /link /SUBSYSTEM:WINDOWS /OUT:C:\Users\%USERNAME%\.claude\cmd.exe Build (MinGW-w64): g++ -O2 -municode -Wl,-subsystem,windows -o C:\Users\%USERNAME%\.claude\cmd.exe hiddencmd.cpp Launcher integration (set COMSPEC before launching Claude Code): $env:COMSPEC = "C:\Users\$env:USERNAME\.claude\cmd.exe" & "C:\path\to\claude.exe" $args ### Why This Works Completely eliminates flash (not just reduces) No admin privileges required Survives Claude Code updates (lives in your launcher) Scoped to Claude Code only (other apps unaffected) Preserves stdout/stderr (hooks can output normally) * ✅ Preserves exit codes ### What Doesn't Work Approach Why It Fails windowsHide: true Window created before option is read NODE_OPTIONS preload Same—too late nodew.exe Can't write to stdout Patching cli.js Reverted on updates, still flashes Job Objects Can't enforce CREATE_NO_WINDOW on children PATH-based shim Windows resolves cmd.exe from System32 first

For me, the fix came when they updated claude-mem plugin.

jthweny · 7 months ago
For me, the fix came when they updated claude-mem plugin.

Is it Claude-mem? - That actually sounds right because it happened when I installed Claude-mem. I can't remember what fixed it completely at first, but all I remember is that I put in this prompt to my AI and then it was fixed in 2 mins.
Either way there has to be a way to use Claude-mem because it didn't get that popular without it working.

jthweny · 7 months ago

It also happened before I installed Claude-mem too, it's actually funny how you mentioned that.

topemalheiro · 7 months ago

Oh, you got it fixed, cool.

github-actions[bot] · 6 months ago

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

github-actions[bot] · 5 months ago

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.