Skip to content

Windows: npm CLI spawns the native binary without windowsHide — a terminal window flashes on every invocation #143

Description

@EvenZ2001

Summary

On Windows, the npm CLI wrapper spawns the native mnemon binary without windowsHide: true. When the parent is a GUI process with no console (e.g. DSH Desktop / Electron calling mnemon status), Windows allocates a new console window for every invocation — visible as a Windows Terminal window that flashes open and closed.

The harness polls mnemon status roughly every 10 seconds, so this flashes repeatedly and continuously during normal use.

Affected files

npm/cli/lib/child.js — used by every runChild() call, i.e. the main path from bin/mnemon.js:

export async function runChild(command, args, options = {}) {
  const child = spawn(command, args, { stdio: "inherit", ...options });
  // ← windowsHide: true is missing

npm/cli/lib/update.js — the mnemon update path:

output(command, args) {
  const result = spawnSync(command, args, { encoding: "utf8" });
  // ← windowsHide: true is missing

bin/mnemon.js resolves the platform binary and delegates to runChild, so this file covers all ordinary subcommands:

const binary = path.join(platformRoot, target.binary);
settle(await runChild(binary, args));

Environment

OS Windows 11 Education 10.0.26200
Terminal host Windows Terminal 1.24.11911.0 (default on Win11)
@mnemon-dev/mnemon 0.2.8
Node via the DSH Desktop 2.0.10 Electron binary
Consumer dsh-mnemon 0.5.10 (calls mnemon … --store default status)

Steps to reproduce

  1. Install @mnemon-dev/mnemon globally on Windows 11 (Windows Terminal as default host).
  2. From a GUI process that owns no console (Electron app, pythonw.exe, or a harness like DSH Desktop), spawn:
    node <npm-root>/@mnemon-dev/mnemon/bin/mnemon.js --data-dir %USERPROFILE%\.mnemon --store default status
    
  3. Observed: a terminal window appears and closes instantly, on every invocation. An agent harness that polls status every ~10 s flashes continuously.
  4. Expected: no window; the CLI is a background tool.

Root-cause proof

windowsHide: true is what makes Node pass CREATE_NO_WINDOW to CreateProcess. Without it, a console-subsystem child of a console-less parent gets a brand-new visible console.

Process-creation trace of the real call path (harness → wrapper → native binary):

DSH Desktop.exe
  → node .../@mnemon-dev/mnemon/bin/mnemon.js --data-dir C:\Users\...\.mnemon --store default status
    → mnemon.exe --data-dir C:\Users\...\.mnemon --store default status
      → conhost.exe 0x4
        → OpenConsole.exe -Embedding
          → WindowsTerminal.exe -Embedding        ← the visible flash

Repeated at 16:18:31 → 16:18:41 → 16:18:53 → 16:19:02 (≈ every 10 s).

Controlled A/B from a console-less parent (pythonw.exe), running git --version and enumerating windows with EnumWindows (same spawn semantics as child.js):

Without windowsHide (current code):

NEW terminal windows: 2
  class=CASCADIA_HOSTING_WINDOW_CLASS visible=True pid=2772  title='D:\\Git\\cmd\\git.exe'
  class=PseudoConsoleWindow              visible=True pid=19800 title=''

With windowsHide: true:

NEW terminal windows: 0
  (no window: child ran without a visible console)

A foreground-window poll (GetForegroundWindow every 80 ms) also caught the flash directly:

FOCUS -> WindowsTerminal [...|CASCADIA_HOSTING_WINDOW_CLASS|...\@mnemon-dev\mnemon-win32-x64\bin\mnemon.exe]

Suggested fix

-  const child = spawn(command, args, { stdio: "inherit", ...options });
+  const child = spawn(command, args, { stdio: "inherit", windowsHide: true, ...options });
-      const result = spawnSync(command, args, { encoding: "utf8" });
+      const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });

Note on ordering in child.js: windowsHide is placed before ...options so a caller may still override it, matching how stdio is already arranged.

windowsHide is a documented no-op on POSIX, so this is safe cross-platform.

Verification after patching the installed package

  • node --check passes on both patched files; ESM import of child.js resolves.
  • End-to-end: ran the exact mnemon status command chain 4× → 0 new terminal windows (previously 1 per run).

Notes

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions