Skip to content

fix(windows): stop console windows from processes this app starts - #180

Merged
ImZoomBoy merged 4 commits into
mainfrom
fix/hidden-console-spawns
Oct 14, 2026
Merged

ImZoomBoy merged 4 commits into
mainfrom
fix/hidden-console-spawns

Conversation

@ImZoomBoy

@ImZoomBoy ImZoomBoy commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Problem

On Windows, a black console window flashes while an agent works and when a message is sent. On this PC new consoles open in Windows Terminal, so each flash is a Terminal window.

What makes the windows

I recorded every visible console window for about an hour while sessions worked, and tied each one to the process that opened it. The last round used the event-hook watcher from First Mate #596, which catches a window that lives for 15 ms. Sources:

Windows Source Where the fix belongs
16 GitHub CLI 2.92.0. gh sometimes starts a detached gh send-telemetry child, which has no console. The tzutil /g it runs then opens a new visible one. 10 were under fm-daemon, 3 under this app's own pull request status gh calls, and 3 under the gh-axi SessionStart hook of a new Claude session (the flash after a sent message). This app's own gh calls: fixed here. fm-daemon: First Mate #596. The hook: the user's hook setup.
14 Playwright's bundled ffmpeg-win64.exe, from agents' browser tests (playwright-cli video-start). Open 6 to 12 seconds each. Playwright CLI, and the worker instructions that use it. Not this repository.
12 node.exe from apps/server/src/provider/testFixtures/sessionProcessTree.mjs, 6 per run of sessionProcessJob.test.ts. Fixed here.
1 powershell Start-Process cmd from another session. Not this repository.

The processes this app starts itself (git, gh, taskkill, claude) are hidden by the existing hook in packages/shared/src/windowsConsole.ts. The Claude Agent SDK also starts claude with windowsHide: true. Claude's children (hooks, MCP servers such as rea.mjs mcp and cmd.exe /c npx) get their own hidden consoles and drew nothing.

Playwright and ffmpeg

I ran each case from my copy with a made-up page, under the watcher. I ran each once from a hidden console (as an agent's shell tool runs it) and once from a parent with no console (as a detached daemon runs it):

Case Hidden console parent No console parent
ffmpeg started by a script with Node's defaults 0 windows 1 (ffmpeg-win64.exe < node.exe)
Headless browser alone (chromium.launch) 0 1 (chrome-headless-shell.exe < node.exe)
Playwright recordVideo 0 2 (chrome-headless-shell.exe and ffmpeg-win64.exe, both < node.exe)

So the window comes from a parent with no console, not from ffmpeg or the browser as such. Playwright CLI (@playwright/cli 0.1.19, playwright-core 1.63 alpha) starts its session daemon with detached: true (lib/tools/cli-client/session.js and program.js). On Windows that leaves the daemon with no console. Every browser and ffmpeg it starts then gets a visible one. This repository starts neither: its preview recording uses the in-window screencast, not a child process.

What this fixes in this repository

  1. Every gh the server starts runs with GH_TELEMETRY=0. All of them go through VcsProcess.run, so the variable is set there, for gh only. gh help environment documents false or 0 as off. The user's own gh configuration is not touched.
  2. The hook missed console programs found only on the child's own PATH. It resolved a bare name against the server's PATH, but Windows searches the child's. A program it could not find was left alone and drew a window. The device host does this for adb: LocalDeviceHost.ts adds the Android SDK folders to the child's PATH and runs adb by name. The hook now reads PATH and PATHEXT from the child's environment.
  3. taskkill ran twice on every child that exited non-zero. Effect's spawner ran taskkill /pid <pid> /T /F from the exit listener and again from the scope release. The process was already gone, so both failed with "process not found". Each run also started its own console host, and by then the pid could belong to an unrelated process. Effect 4.0.4 fixed this by skipping taskkill once the leader has exited. This adds a pnpm patch that backports that guard to @effect/platform-node-shared 4.0.0-rc.115.
  4. The sessionProcessJob test fixture opened console windows. Its dev server starts detached, so the watcher child opened a visible console. The fixture now starts its processes hidden.

Measurements

Event-hook watcher, every case started from a parent with no console, as the desktop backend runs.

main this branch
200 gh api rate_limit calls through the real VcsProcess 3 telemetry senders, 3 windows 0 senders, 0 windows
Child-PATH probe: a console program on the child's PATH only, 2 runs 1 window per run 0
taskkill runs under my server during two Haiku turns 37 0 to 1
Visible windows under my server during two Haiku turns 1 (gh telemetry under the Claude hook) 0, in two runs
sessionProcessJob.test.ts run 6 windows 0

During the "after" gh run the watcher saw one telemetry window. Its parent was the live fm-daemon (gh api ... pulls/594), which does not have First Mate #596 yet. Server turns used an empty temp home and two cheap prompts: "Reply with just the word ok." and one that runs git status.

Tests

  • apps/server/src/vcs/VcsProcess.test.ts: every gh gets GH_TELEMETRY: "0", a caller's own variables are kept, and git is untouched. Before the change it failed with expected [ undefined, { GH_DEBUG: '' }, undefined ].
  • packages/shared/src/windowsConsole.test.ts: a console child found only on the PATH its caller passed is hidden. On the old hook it fails with expected false to be true.
  • apps/server/src/process/taskkillAfterExit.test.ts, on Windows only, like the real-process cases in sessionProcessJob.test.ts. A child that exits 3 starts no taskkill, and a running child stopped by closing its scope starts exactly one. Without the patch both fail with [ 'taskkill', 'taskkill' ].
  • The fixture change has no test of its own. Whether a console gets a window depends on the terminal host, which Node cannot see, so the watcher numbers above are its evidence. sessionProcessJob.test.ts still passes (10 of 10).

Fork notes

  • windowsConsole.ts and the fixture are covered by existing fork-features.json entries (ours whole). New entries: gh-telemetry-off (VcsProcess.ts, with its named test) and effect-taskkill-after-exit (the patch and its pnpm-workspace.yaml line).
  • Upstream main is on Effect 4.0.4, which carries the taskkill guard. Upstream has no GH_TELEMETRY change. Upstream v0.0.45 is still on rc.115, so the sync/upstream-v0.0.45 merge (chore: merge upstream v0.0.45 #179) needs this patch carried as is. pnpm-lock.yaml may conflict on the patch hash lines, and regenerating the lock settles it. Drop the patch when the fork moves to Effect 4.0.4.

Checks run on Windows

$ (cd packages/shared && vp run typecheck)    exit=0
$ (cd apps/server && vp run typecheck)        exit=0 (suggestions only, 0 "error TS")
$ vp lint --report-unused-disable-directives  exit=0 (warnings only, none in touched files)
$ vp fmt --check                              exit=0
$ vp run knip:check                           exit=0
$ node scripts/check-fork-features.ts
Fork features: 39 entries, every file and test present.
 Test Files  1 failed | 36 passed (37)
      Tests  11 failed | 636 passed (647)

The 11 failures are scripts/build-desktop-artifact.test.ts, which fails the same on main on this machine (Tests 11 failed | 60 passed (71)).

Test suites of the touched packages:

packages/shared               Tests  1 failed | 897 passed | 7 skipped (905)
apps/server src/provider      Tests  2 failed | 1546 passed | 52 skipped (1600)
apps/server src/orchestration Tests  767 passed (767)
apps/server git, vcs, process, terminal, checkpointing, sourceControl, pullRequest, workspace, project
                              Tests  3 failed | 1687 passed | 25 skipped (1715)
apps/server vcs, sourceControl, pullRequest (after the GH_TELEMETRY commit)
                              Tests  2 failed | 1190 passed | 1 skipped (1193)
apps/server assets ... persistence
                              Tests  6 failed | 700 passed | 16 skipped (722)
apps/server remaining dirs and top-level tests
                              Tests  5 failed | 771 passed | 37 skipped (813)
apps/server integration       Tests  42 passed | 1 skipped (43)
scripts/check-fork-features.test.ts  Tests  12 passed (12)

All 16 apps/server failures fail the same on main on this machine, run as the same files: Tests 16 failed | 297 passed | 12 skipped (325). They are in OpenCodeProvider, userInputAttachments, AgentSessionScanner, GitVcsDriver, GitVcsDriverCore, update, bootService, LocalDeviceHost (a path separator assertion), McpHttpServer and serviceLauncher. The packages/shared failure is nodeRuntime.test.ts > rejects a hard-linked node alias, which is flaky here and failed 7 of 8 runs on main.

autoprintworks added 3 commits October 14, 2026 08:23
The Windows console hook resolved a bare program name against the server's
own PATH. Windows searches the child's PATH. When a caller adds a directory
only the child sees, the hook could not find the program, left it alone, and
the program opened a visible console window. The device host does this for
adb, adding the Android SDK to the child's PATH.

The hook now resolves with the PATH and PATHEXT of the child's environment.
On Windows, Effect's spawner ran `taskkill /pid <pid> /T /F` twice for
every child that exited non-zero: once from the exit listener and once from
the scope release. The process was already gone, so each run failed with
"process not found", and each started its own console host. The pid could
also belong to an unrelated process by then.

Patch @effect/platform-node-shared 4.0.0-rc.115 with the guard Effect 4.0.4
ships: no taskkill once the leader has exited. Drop the patch when the fork
moves to Effect 4.0.4 or later.
…e windows

The fixture's dev server starts detached, so it has no console. On Windows
the watcher it starts then opened a visible console window, several times
per run of sessionProcessJob.test.ts. Start every fixture process hidden.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 14, 2026
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.5 KiB 13.5 KiB +14 B (+0.1%) 15.1 KiB ✅
Codex Thread snapshot wire 7.1 KiB 7.1 KiB −1 B (−0.0%) 7.3 KiB ✅
Codex Live turn WebSocket wire 6.4 KiB 6.4 KiB +15 B (+0.2%) 7.8 KiB ✅
Codex Live turn WebSocket decoded 56.1 KiB 56.1 KiB 0 B (0.0%) 66.4 KiB ✅
Codex Live turn messages 7 7 0 (0.0%) 21 ✅
Claude Total thread wire 13.5 KiB 13.5 KiB +2 B (+0.0%) 15.1 KiB ✅
Claude Thread snapshot wire 7.1 KiB 7.1 KiB +3 B (+0.0%) 7.3 KiB ✅
Claude Live turn WebSocket wire 6.4 KiB 6.4 KiB −1 B (−0.0%) 7.8 KiB ✅
Claude Live turn WebSocket decoded 56.9 KiB 56.9 KiB 0 B (0.0%) 66.4 KiB ✅
Claude Live turn messages 7 7 0 (0.0%) 21 ✅

Baseline: af3ef03 · PR result: 42d3152 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 114.0 KiB
  • Claude decoded thread snapshot: 114.7 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

gh 2.92 can start a detached `gh send-telemetry` child. On Windows that
child has no console, so the `tzutil /g` it runs opened a visible console
window. The pull request status checks hit this a few times an hour.

VcsProcess, which every gh call goes through, now sets GH_TELEMETRY=0 for
gh. `gh help environment` documents 0 as off. The user's own gh
configuration is not touched.
@ImZoomBoy ImZoomBoy changed the title fix(windows): stop console windows from spawns this app starts fix(windows): stop console windows from processes this app starts Oct 14, 2026
@ImZoomBoy

Copy link
Copy Markdown
Owner Author

Review - spec and standards

One independent read-only review of 42d3152, both axes. Verdict: no blocking finding. The reviewer ran the telemetry test against the old VcsProcess.ts and saw it fail, and checked the Effect patch against the 4.0.4 guard. The low-severity points below are left as notes.


PR 180 review: no blocking findings.
CHECKED SOUND: Effect patch matches 4.0.4 guard exactly (unpkg source), applied by vp i, kept by CLI archive (direct server dep). Every gh call goes through VcsProcess.run (GitHubCli, githubStackActions, measure-pr-preview.ts); extendEnv merges env so PATH kept; git untouched. Telemetry test passes on branch and FAILS with main's VcsProcess.ts (ran it). taskkill test passes on branch. Fixture windowsHide placed before ...options, job proof intact. Branch fix/hidden-console-spawns, no em dashes, no agent names.
(a) SPEC GAPS: 1) AC4 'keep line naming every file' - pnpm-lock.yaml changed, named by no entry (body says regenerate). high conf / low sev. 2) No test covers taskkill or fixture windows directly; body explains why. medium / low.
(b) SCOPE: none beyond task.
(c) LOOKS WRONG: 1) windowsConsole.ts:216 cache key lacks PATHEXT, now per-child. high / low. 2) child env without PATHEXT falls back to DEFAULT_PATHEXT, not server's. medium / low. 3) envPairs parsed on every spawn (Node always fills it); cheap but new. high / low. 4) quoted PATH entries not unquoted (pre-existing). medium / low. 5) GH_TELEMETRY=0 overrides a user's deliberate env value; defensible. high / low.
(d) STANDARDS: 1) upstream-conflict-resolution.md 'Every entry names a test that cannot pass on plain upstream' - effect-taskkill-after-exit's test passes on upstream main (4.0.4 has guard) and is win32-only so skips on Linux CI. high / MEDIUM. 2) keep line calls pnpm-workspace.yaml part 'ours whole'; it is ours-in-part shape. medium / low. 3) docs/internals/windows-console-windows.md edited line over wrap width. high / cosmetic.
(e) SMELLS: 1) taskkill test monkey-patches ChildProcess.prototype.spawn (internal), matches windowsConsole.ts pattern. low. 2) check-fork-features output shows '1 failed' with no exit code; attributed to main-baseline build-desktop-artifact.test.ts, not reproduced by me. low. 3) Playwright detached:true and PR 179 carry claims not verified; plausible, no code here.
No helpers used. Copy clean (restored VcsProcess.ts; vp i added only ignored node_modules).

@ImZoomBoy
ImZoomBoy merged commit 87cd1fe into main Oct 14, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant