Skip to content

[Windows] Three detached: true spawns missing windowsHide — telemetry flushSync opens a console window on nearly every CLI exit #3476

Description

@13aherbert

Describe the bug

On Windows, console windows pop up during ordinary CLI use — not just during renders. This is a follow-up to #3379 (ffmpeg, fixed) and is distinct from #3430 (chrome-headless-shell, still open): those two cover console-subsystem binaries, whereas this one is about detached: true spawns of Node itself.

Node's docs are explicit about the mechanism:

On Windows, setting options.detached to true makes it possible for the child process to continue running after the parent exits. The child will have its own console window.

child_process.spawn() defaults windowsHide to false, so any detached: true spawn opens a visible console window on Windows unless windowsHide: true is passed.

Root cause

packages/cli v0.8.13 has four detached: true spawns. Three are missing windowsHide: true:

Source What it spawns windowsHide
src/telemetry/transport.ts (flushSync) node -e "fetch(...)" → PostHog ❌ missing
src/utils/openBrowser.ts (openBrowser) user's browser ❌ missing
src/commands/previewLifecycle.ts background preview server ❌ missing
src/utils/autoUpdate.ts update check ✅ present

autoUpdate.ts already gets this right, which makes the other three look like oversights rather than intent.

The worst offender: telemetry flushSync

flushSync() is the process-exit flush path. Because it can't await an HTTP POST during shutdown, it hands the payload to a detached Node process to deliver after the parent dies:

function flushSync() {
  const payload = buildPayload(eventQueue);
  if (payload == null) return;
  eventQueue = [];
  try {
    const child = spawn(
      process.execPath,
      ["-e", `fetch(${JSON.stringify(`${POSTHOG_HOST}/batch/`)},{...})`],
      { detached: true, stdio: "ignore" }   // <-- no windowsHide
    );
    child.unref();
  } catch {}
}

The strategy itself is sound — the only thing missing is windowsHide: true.

Impact is amplified by how the CLI is actually used. Agent-driven workflows invoke many short-lived commands (check, lint, keyframes, capture, snapshot, beats, preview), and each invocation that exits with queued events opens its own console window. My ~/.hyperframes/config.json shows commandCount: 1350, which is a lot of windows. Unlike #3430 this isn't render-only — it fires on nearly every command, including read-only ones.

Steps to reproduce

  1. On Windows, ensure telemetry is on (default): telemetryEnabled: true in ~/.hyperframes/config.json
  2. Run any short command, e.g. npx hyperframes check . or npx hyperframes lint .
  3. Watch the desktop/taskbar as the command exits — a console window flashes open
  4. Run several commands in sequence; you get one window per exit

Setting HYPERFRAMES_NO_TELEMETRY=1 (or DO_NOT_TRACK=1) makes the flashing stop, which isolates the telemetry spawn as the cause.

For the other two sites: hyperframes preview without --foreground spawns the detached background server (one window), and without --no-open spawns the browser via options.browserPath (one more).

Expected behavior

No console window appears for any of these background spawns.

Actual behavior

A console window opens per detached spawn — in the telemetry case, on nearly every CLI invocation.

Suggested fix

Add windowsHide: true at the three call sites, matching what autoUpdate.ts already does:

// src/telemetry/transport.ts
const child = spawn(process.execPath, ["-e", script], {
  detached: true,
  stdio: "ignore",
  windowsHide: true,
});

// src/utils/openBrowser.ts
const child = spawn7(options.browserPath, args, {
  detached: true,
  stdio: "ignore",
  windowsHide: true,
});

// src/commands/previewLifecycle.ts
child = spawn17(execPath, args, {
  detached: true,
  stdio: ["ignore", logFd, logFd],
  env: process.env,
  windowsHide: true,
});

windowsHide is a no-op on macOS/Linux, so it's safe to apply unconditionally — same rationale as the #3379 fix.

Given that #3379, #3430, and this issue are all the same class of defect, it may be worth a small shared spawn helper that applies windowsHide: true by default, plus a lint rule that flags a bare detached: true without it.

Environment

hyperframes  0.8.13 (latest)
Node.js      v24.15.0 (win32 x64)
OS           Microsoft Windows 10 Home 10.0.19045 (build 19045)
Install      npx (_npx cache)

Additional context

Verified by static analysis of the shipped dist/cli.js in hyperframes@0.8.13 — all four detached: true sites were inspected directly, along with every windowsHide occurrence. All ffmpeg/ffprobe spawns correctly carry windowsHide: true with the explanatory comment added by the #3379 fix, so that regression has not returned.

Related: #3379 (ffmpeg, closed/fixed) · #3430 (chrome-headless-shell, open)

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