[Windows] #3379's windowsHide fix landed in engine only — studio-server, lint and cli still spawn console binaries unhidden (fires during preview/Studio, not just render)

Author: BenT034Created Aug 26, 2026Updated Sep 15, 2026

Describe the bug

Follow-up to #3379 (ffmpeg console windows, fixed) — the fix landed in packages/engine only. Console-subsystem binaries are still spawned without windowsHide: true from studio-server, lint and several cli paths, so on Windows console windows still pop up. Distinct from #3476 (detached: true spawns of Node itself) and #3430 (chrome-headless-shell).

The practical difference from #3379: those were render-time. These fire during ordinary Studio use. previewLifecycle starts the preview server with detached: true, so that server has no console of its own. Every console-subsystem child it spawns therefore gets a brand-new visible console window from Windows for the duration of the call — an ffprobe flash of ~100–400 ms, a waveform or proxy encode for seconds. Scrubbing a timeline or opening the colour-grading panel is enough to trigger it.

packages/engine/src/utils/runFfmpeg.ts already carries the explanatory comment after #3379:

windowsHide: ffmpeg/ffprobe are console-subsystem binaries, so without this Node opens a visible console window per spawn on Windows.

That reasoning applies verbatim to the sites below.

Root cause

Verified by reading the shipped bundle of 0.8.15 (node_modules/hyperframes/dist/cli.js) — none of these pass windowsHide:

dist/cli.js spawns options actually passed
:68030 execFileP(getFfmpegBinary(), ["-hide_banner","-filters"]) { maxBuffer, timeout }
:102626 spawn(ffmpegPath, ["-hide_banner","-filters"]) { stdio }
:100507 execFileSync("taskkill", …)inside a process.platform === "win32" branch { stdio, timeout }
:100542 execFileSync("powershell.exe", ["-NoProfile","-NonInteractive","-Command", …])win32-only branch { encoding, timeout }
:100571 execFileSync("powershell.exe", …)win32-only branch { encoding, timeout }

The two powershell.exe calls and the taskkill call sit behind explicit process.platform === "win32" guards, i.e. they exist only to run on Windows, and still omit the one option that matters there. getProcessIdentity/getParentPid run one PowerShell per ancestry hop, so a single hyperframes preview --status / --list / --stop spawns several.

Corresponding sources (paths from a main checkout):

  • packages/studio-server/src/helpers/mediaMetadata.tsexecFile(ffprobe, …); called on preview-HTML build and by GET /api/projects/:id/media/metadata, which is uncached (one ffprobe per request) and is hit by the colour-grading panel
  • packages/studio-server/src/helpers/waveform.tsspawn(ffmpeg, …) for audio peaks
  • packages/studio-server/src/helpers/proxyTranscoder.ts — the -filters HDR probe and the proxy transcode
  • packages/studio-server/src/helpers/mediaValidation.tsspawnSync("ffprobe", …) on upload, no options at all
  • packages/lint/src/hevcPreviewLint.ts — one ffprobe per video asset, per lint run
  • packages/cli/src/utils/orphanCleanup.tstaskkill, powershell.exe ×2
  • packages/cli/src/browser/manager.tsexecSync("where chrome")cmd.exe
  • packages/cli/src/capture/captureCompositionFrame.tsspawn(ffmpegPath, args) with no options object
  • packages/cli/src/background-removal/pipeline.ts, packages/cli/src/browser/ffmpeg.ts, packages/cli/src/utils/webmAlphaCheck.ts, packages/cli/src/browser/preflight.ts

Steps to reproduce

  1. On Windows, in any project with a video asset: npx hyperframes preview --background
  2. Open the Studio, scrub the timeline, open the colour-grading panel
  3. Watch the desktop — console windows flash open and closed
  4. Also visible with just npx hyperframes preview --status (PowerShell spawns from orphanCleanup)

Expected behavior

No visible console windows during preview/Studio use, matching the post-#3379 behaviour of render.

Actual behavior

Console windows flash open and closed repeatedly while the Studio is open, and on every preview --status/--list/--stop.

Suggested fix

Add windowsHide: true at each site (a no-op on macOS/Linux, so it can be applied unconditionally). For the calls that pass no options object at all, add one.

It may be worth adding a lint rule or a shared spawnHidden() helper so this class of regression stops recurring — this is now the third issue in the same family (#3379, #3476, this one).

Two smaller things noticed while tracing, mentioned only because they are in the same code paths and not filed separately:

  • routes/preview.ts calls probeAssetCodec(file) per ?hf-proxy= asset request, deliberately before the 304 shortcut, and routes/media.ts GET …/media/metadata has no cache — both are uncapped ffprobe-per-request paths.
  • packages/cli/src/utils/autoUpdate.ts passes detached: true and windowsHide: true, which is the combination Windows resolves in favour of DETACHED_PROCESS (documented: CREATE_NO_WINDOW is ignored when DETACHED_PROCESS is set). Same conflict as #3430's root cause, so the windowsHide there is not doing what it looks like it does.

Environment

Version          0.8.15 (latest)
Node.js          v22.23.2 and v24.19.0 (win32 x64) — reproduced on both
OS               Windows 11 Home 10.0.26200
CPU              AMD Ryzen 7 5700U
FFmpeg           9.0 (Gyan build, winget)