{"repo":"NousResearch/hermes-agent","public_id":"24073","display_label":"Windows console/terminal flash suppression fixes","summary":"This cluster is focused on a single theme: preventing unwanted console window popups and flashes on Windows when background subprocesses are spawned. Members include bug reports, feature requests, and numerous incremental fixes targeting subprocess creation flags like CREATE_NO_WINDOW and CREATE_BREAKAWAY_FROM_JOB across various spawn sites (git, ffmpeg, netstat, gateway, etc.).","subsystem":"apps/desktop","counts":{"members":45,"open_issues":0,"open_issues_distinct":0,"open_prs":2,"closed_issues":24,"merged_prs":4,"attempt_prs":17},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":54417,"merged_at":"2026-06-28T20:49:09+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-28T20:49:09+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.042020202020202},"fix_edges":[{"issue_number":52310,"pr_number":53810,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-27T21:02:24+00:00"},{"issue_number":53016,"pr_number":53892,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T16:16:36+00:00"},{"issue_number":53016,"pr_number":54236,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T12:28:46+00:00"},{"issue_number":53016,"pr_number":54417,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T20:49:09+00:00"},{"issue_number":53053,"pr_number":53892,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T16:16:36+00:00"},{"issue_number":53053,"pr_number":54236,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T12:28:46+00:00"},{"issue_number":53053,"pr_number":54417,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T20:49:09+00:00"},{"issue_number":53178,"pr_number":54236,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T12:28:46+00:00"},{"issue_number":53282,"pr_number":54236,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T12:28:46+00:00"},{"issue_number":53282,"pr_number":54417,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T20:49:09+00:00"},{"issue_number":53539,"pr_number":53892,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T16:16:36+00:00"},{"issue_number":53539,"pr_number":54417,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T20:49:09+00:00"},{"issue_number":54364,"pr_number":54417,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-28T20:49:09+00:00"}],"members":[{"kind":"issue","number":24073,"title":"[Feature]: Hidden Terminal exec on Windows Native","url":null,"state":"closed"},{"kind":"issue","number":42544,"title":"Windows: terminal tool flashes cmd window on every command execution","url":null,"state":"closed"},{"kind":"issue","number":43848,"title":"[Feature]: Set the \"CREATE_NO_WINDOW\" flag for Window terminal process","url":null,"state":"closed"},{"kind":"issue","number":49851,"title":"Windows: terminal tool creates visible bash/cmd windows that steal focus","url":null,"state":"closed"},{"kind":"issue","number":52310,"title":"[Bug]: Black console window flashes briefly whenever terminal spawns bash on Windows","url":null,"state":"closed"},{"kind":"issue","number":52788,"title":"bug(desktop): CMD console window flashes on every terminal command on Windows","url":null,"state":"closed"},{"kind":"issue","number":52981,"title":"[Bug]: Windows 11 Hermes Desktop — 频繁弹出黑色控制台窗口闪烁（git.exe / gh.exe / cmd.exe）","url":null,"state":"closed"},{"kind":"issue","number":52982,"title":"[Bug]: Windows 11 Hermes Desktop — 频繁弹出黑色控制台窗口闪烁（git.exe / gh.exe / cmd.exe）","url":null,"state":"closed"},{"kind":"issue","number":53014,"title":"Windows: terminal 工具强制使用 bash 导致频繁弹窗","url":null,"state":"closed"},{"kind":"issue","number":53016,"title":"[Bug] Windows desktop app flashes cmd console window intermittently after update","url":null,"state":"closed"},{"kind":"issue","number":53053,"title":"[Bug]: Subprocess calls flash black cmd.exe windows on Windows","url":null,"state":"closed"},{"kind":"issue","number":53273,"title":"Windows Desktop GUI: CREATE_NO_WINDOW insufficient — cmd windows flash on every terminal() call","url":null,"state":"closed"},{"kind":"issue","number":53282,"title":"Windows: Console window flashes on every subprocess call (missing CREATE_NO_WINDOW)","url":null,"state":"closed"},{"kind":"issue","number":53342,"title":"[Bug]: The Hermes desktop client on Windows keeps flickering black command prompt windows nonstop, making the program completely inoperable. This is a critical blocking bug!","url":null,"state":"closed"},{"kind":"issue","number":53424,"title":"[Bug] Windows: rapid conhost.exe window flashing due to missing CREATE_NO_WINDOW in subprocess calls","url":null,"state":"closed"},{"kind":"issue","number":53751,"title":"Windows 10 desktop app triggers infinite PowerShell flash loop on launch","url":null,"state":"closed"},{"kind":"issue","number":54323,"title":"windows_hide_flags() missing CREATE_BREAKAWAY_FROM_JOB — Desktop app console flash persists after #54236","url":null,"state":"closed"},{"kind":"issue","number":54506,"title":"Command prompt keep blinking at screen when desktop app is open","url":null,"state":"closed"},{"kind":"issue","number":55604,"title":"windows_hide_flags() missing CREATE_BREAKAWAY_FROM_JOB — cmd.exe windows flash in Electron desktop apps (Hermes One, etc.)","url":null,"state":"closed"},{"kind":"issue","number":55627,"title":"[Bug]: OpenConsole.exe / conhost.exe / git.exe 进程无限闪烁弹窗 — Windows Electron 桌面版","url":null,"state":"closed"},{"kind":"issue","number":55633,"title":"[Bug]: OpenConsole.exe / conhost.exe / git.exe processes infinitely flash and spawn — Windows Electron Desktop","url":null,"state":"closed"},{"kind":"issue","number":56503,"title":"Windows subprocess console flashes — request for official fix","url":null,"state":"closed"},{"kind":"issue","number":56747,"title":"[Windows] Blank terminal console windows flash when running agent or tools on desktop GUI","url":null,"state":"closed"},{"kind":"issue","number":63698,"title":"[Bug]: Console windows flash/appear despite windows_hide_console: true","url":null,"state":"closed"},{"kind":"pr","number":42605,"title":"fix(terminal): prevent Windows cmd flash (#42544)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"aa2ae36c3","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":44105,"title":"fix(windows): hide console-window flash on gateway-runtime subprocess spawns","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":49931,"title":"fix(terminal): bypass Git Bash wrapper to prevent console flashes on Windows","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":50183,"title":"fix(terminal): suppress console window flash on Windows via CREATE_NO_WINDOW (#49851, #42544)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"aa2ae36c3","number":0},{"kind":"pr","value":"#54220","number":54220},{"kind":"pr","value":"#42544","number":42544},{"kind":"pr","value":"#49851","number":49851}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":53119,"title":"fix: add CREATE_NO_WINDOW to Windows subprocess Popen calls to prevent cmd.exe flashing","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54220","number":54220},{"kind":"pr","value":"#54236","number":54236},{"kind":"pr","value":"#53892","number":53892},{"kind":"pr","value":"#54417","number":54417}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":53123,"title":"fix(windows): suppress console-window flashes from gh/git probes in desktop gateway","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53844","number":53844},{"kind":"pr","value":"#53791","number":53791},{"kind":"pr","value":"#53810","number":53810},{"kind":"pr","value":"#53829","number":53829}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":53291,"title":"fix: suppress console window flashes on Windows via global subprocess patch (#53282)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54220","number":54220},{"kind":"pr","value":"#54236","number":54236},{"kind":"pr","value":"#53892","number":53892},{"kind":"pr","value":"#54417","number":54417}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":53344,"title":"fix(desktop): avoid uv venv pythonw launcher console flash on Windows","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":53810,"title":"fix(windows): stop terminal-window popups from background spawns","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/main.cjs","cron/scheduler.py","gateway/run.py","gateway/status.py","hermes_bootstrap.py","hermes_cli/_subprocess_compat.py","hermes_cli/claw.py","hermes_cli/clipboard.py","hermes_cli/gateway.py","hermes_cli/gateway_windows.py","hermes_cli/main.py","hermes_cli/web_server.py","hermes_constants.py","plugins/platforms/discord/adapter.py","plugins/platforms/whatsapp/adapter.py","tests/gateway/test_status.py","tests/hermes_cli/test_claw.py","tests/hermes_cli/test_gateway.py","tests/hermes_cli/test_gateway_windows.py","tests/hermes_cli/test_web_server.py","tests/test_hermes_bootstrap.py","tests/test_hermes_constants.py","tests/test_no_visible_console_spawns.py","tests/tools/test_browser_hardening.py","tests/tools/test_windows_native_support.py","tools/browser_tool.py","tools/environments/local.py","tools/process_registry.py","tools/transcription_tools.py","tools/tts_tool.py","tui_gateway/entry.py","tui_gateway/git_probe.py","tui_gateway/slash_worker.py"],"fix_hunks":[{"file":"apps/desktop/electron/main.cjs","hunks":["@@ -1565,18 +1565,17 @@ function readVenvHome(venvRoot) {\n function getNoConsoleVenvPython(venvRoot) {\n   if (!IS_WINDOWS) return getVenvPython(venvRoot)\n \n-  // Prefer the venv's own pythonw shim — it carries pyvenv.cfg / site-packages\n-  // wiring. Falling back to the base uv/python.org pythonw.exe skips the venv\n-  // and breaks imports (yaml, hermes_cli, …) even when PYTHONPATH is patched.\n-  const venvPythonw = path.join(venvRoot, 'Scripts', 'pythonw.exe')\n-  if (fileExists(venvPythonw)) return venvPythonw\n-\n+  // uv venv launchers can re-exec console python.exe, which allocates conhost /\n+  // Windows Terminal. Use base pythonw directly and provide imports via env.\n   const baseHome = readVenvHome(venvRoot)\n   if (baseHome) {\n     const basePythonw = path.join(baseHome, 'pythonw.exe')\n     if (fileExists(basePythonw)) return basePythonw\n   }\n \n+  const venvPythonw = path.join(venvRoot, 'Scripts', 'pythonw.exe')\n+  if (fileExists(venvPythonw)) return venvPythonw\n+\n   return venvPythonw\n }\n ","@@ -2797,7 +2796,7 @@ function createPythonBackend(root, label, dashboardArgs, options = {}) {\n     args: ['-m', 'hermes_cli.main', ...dashboardArgs],\n     env: buildDesktopBackendEnv({\n       hermesHome: HERMES_HOME,\n-      pythonPathEntries: [root],\n+      pythonPathEntries: [root, ...getVenvSitePackagesEntries(venvRoot)],\n       venvRoot\n     }),\n     root,","@@ -2821,7 +2820,7 @@ function createActiveBackend(dashboardArgs) {\n     args: ['-m', 'hermes_cli.main', ...dashboardArgs],\n     env: buildDesktopBackendEnv({\n       hermesHome: HERMES_HOME,\n-      pythonPathEntries: [ACTIVE_HERMES_ROOT],\n+      pythonPathEntries: [ACTIVE_HERMES_ROOT, ...getVenvSitePackagesEntries(VENV_ROOT)],\n       venvRoot: VENV_ROOT\n     }),\n     root: ACTIVE_HERMES_ROOT,"]},{"file":"cron/scheduler.py","hunks":["@@ -3046,4 +3046,11 @@ def _on_done(_f: concurrent.futures.Future) -> None:\n \n \n if __name__ == \"__main__\":\n+    # Standalone background scheduler: drop any console a uv pythonw→python\n+    # re-exec auto-allocated. No-op on POSIX / when run in-gateway.\n+    try:\n+        import hermes_bootstrap\n+        hermes_bootstrap.detach_orphan_console()\n+    except Exception:\n+        pass\n     tick(verbose=True)"]},{"file":"gateway/run.py","hunks":["@@ -18593,6 +18593,13 @@ def restart_signal_handler():\n \n def main():\n     \"\"\"CLI entry point for the gateway.\"\"\"\n+    # Background daemon: drop any console auto-allocated by a uv pythonw→python\n+    # re-exec so no terminal lingers. No-op on POSIX / when already detached.\n+    try:\n+        hermes_bootstrap.detach_orphan_console()\n+    except Exception:\n+        pass\n+\n     # Force UTF-8 stdio on Windows — gateway logs and startup banner would\n     # otherwise UnicodeEncodeError on cp1252 consoles.  No-op on POSIX.\n     try:"]},{"file":"gateway/status.py","hunks":["@@ -81,8 +81,10 @@ def terminate_pid(pid: int, *, force: bool = False) -> None:\n     because os.kill(..., SIGTERM) is not equivalent to a tree-killing hard stop.\n     \"\"\"\n     if force and _IS_WINDOWS:\n+        from hermes_cli import _subprocess_compat\n+\n         try:\n-            result = subprocess.run(\n+            result = _subprocess_compat.run(\n                 [\"taskkill\", \"/PID\", str(pid), \"/T\", \"/F\"],\n                 capture_output=True,\n                 text=True,"]},{"file":"hermes_bootstrap.py","hunks":["@@ -80,6 +80,26 @@ def apply_windows_utf8_bootstrap() -> bool:\n     os.environ.setdefault(\"PYTHONUTF8\", \"1\")\n     os.environ.setdefault(\"PYTHONIOENCODING\", \"utf-8\")\n \n+    # Python's platform.win32_ver()/platform.platform() can shell out to\n+    # ``cmd.exe /c ver`` on Windows.  In pythonw-launched background processes\n+    # that still creates a visible terminal handoff on machines where Windows\n+    # Terminal is the default console host.  Disable that subprocess path early.\n+    try:\n+        import platform\n+\n+        def _no_subprocess_syscmd_ver(\n+            system: str = \"\",\n+            release: str = \"\",\n+            version: str = \"\",\n+            *_args,\n+            **_kwargs,\n+        ) -> tuple[str, str, str]:\n+            return system or \"Windows\", release, version\n+\n+        platform._syscmd_ver = _no_subprocess_syscmd_ver  # type: ignore[attr-defined]\n+    except Exception:\n+        pass\n+\n     # 2. Reconfigure the current process's stdio to UTF-8.  Needed\n     #    because os.environ changes don't retroactively rebind sys.stdout\n     #    — those were bound at interpreter startup based on the console","@@ -122,6 +142,44 @@ def apply_windows_utf8_bootstrap() -> bool:\n     return True\n \n \n+def detach_orphan_console() -> bool:\n+    \"\"\"Free a console window that was auto-allocated for this process alone.\n+\n+    Background-only entry points (gateway daemon, dashboard backend, cron\n+    runner, TUI/desktop stdio backends) call this explicitly. uv-created venvs\n+    ship a ``Scripts\\\\pythonw.exe`` redirector that re-execs the *base* console\n+    ``python.exe``; that re-exec allocates its own conhost/Windows Terminal\n+    window even though the launcher wanted no console. We drop it so nothing\n+    lingers.\n+\n+    This is NOT wired into the import-time bootstrap on purpose: the discriminator\n+    (``GetConsoleProcessList() == 1``) cannot tell a phantom console apart from a\n+    user who deliberately opened the *interactive* CLI/TUI in its own fresh\n+    console (double-click, Start-menu shortcut, a ConPTY), since both report a\n+    single attached process with a tty. Intent is only knowable from the entry\n+    point — so only known-background mains call this, never the interactive CLI.\n+\n+    A properly detached daemon (``DETACHED_PROCESS``) has no console at all, so\n+    ``GetConsoleWindow()`` is NULL and this is a no-op. Returns True iff a console\n+    was actually freed. No-op (returns False) on non-Windows.\n+    \"\"\"\n+    if not _IS_WINDOWS:\n+        return False\n+    try:\n+        import ctypes\n+\n+        kernel32 = ctypes.windll.kernel32\n+        if not kernel32.GetConsoleWindow():\n+            return False\n+        buf = (ctypes.c_uint * 4)()\n+        if kernel32.GetConsoleProcessList(buf, 4) == 1:\n+            kernel32.FreeConsole()\n+            return True\n+    except Exception:\n+        pass\n+    return False\n+\n+\n def harden_import_path(src_root: str | None = None) -> None:\n     \"\"\"Stop a package in the current directory from shadowing Hermes modules.\n "]},{"file":"hermes_cli/_subprocess_compat.py","hunks":["@@ -28,12 +28,15 @@\n from __future__ import annotations\n \n import shutil\n+import subprocess\n import sys\n from typing import Sequence\n \n __all__ = [\n     \"IS_WINDOWS\",\n     \"resolve_node_command\",\n+    \"run\",\n+    \"popen\",\n     \"windows_detach_flags\",\n     \"windows_detach_flags_without_breakaway\",\n     \"windows_hide_flags\",","@@ -201,6 +204,44 @@ def windows_hide_flags() -> int:\n     return _CREATE_NO_WINDOW\n \n \n+# -----------------------------------------------------------------------------\n+# The single chokepoint for spawning a process without a console window.\n+# -----------------------------------------------------------------------------\n+\n+\n+def _no_window(kwargs: dict) -> dict:\n+    \"\"\"OR ``CREATE_NO_WINDOW`` into ``creationflags`` on Windows (no-op on POSIX).\n+\n+    Merges rather than overwrites, so a caller that needs detach semantics can\n+    pass ``creationflags=windows_detach_flags()`` and still go through here —\n+    ``CREATE_NO_WINDOW`` is already part of that bundle, so the OR is idempotent.\n+    \"\"\"\n+    if IS_WINDOWS:\n+        kwargs[\"creationflags\"] = kwargs.get(\"creationflags\", 0) | _CREATE_NO_WINDOW\n+    return kwargs\n+\n+\n+def run(cmd, **kwargs):\n+    \"\"\"``subprocess.run`` that never flashes a console window on Windows.\n+\n+    This is the primitive every Hermes spawn of a *console-subsystem* program\n+    (``taskkill``, ``schtasks``, ``agent-browser``, ``git-bash``, version\n+    probes, …) must use. Routing through one function makes \"no visible\n+    terminal\" structural instead of a per-call-site rule that gets forgotten —\n+    which is exactly how cron-driven and future spawns leaked windows before.\n+\n+    Python child processes are additionally covered by the ``FreeConsole``\n+    catch-all in :mod:`hermes_bootstrap`, but native exes can't run that, so the\n+    spawn-time flag here is the only thing that helps them.\n+    \"\"\"\n+    return subprocess.run(cmd, **_no_window(kwargs))\n+\n+\n+def popen(cmd, **kwargs):\n+    \"\"\"``subprocess.Popen`` counterpart of :func:`run` — see its docstring.\"\"\"\n+    return subprocess.Popen(cmd, **_no_window(kwargs))\n+\n+\n def windows_detach_popen_kwargs() -> dict:\n     \"\"\"Return a dict of Popen kwargs that detach a child on Windows and\n     fall back to the POSIX equivalent (``start_new_session=True``) on"]},{"file":"hermes_cli/claw.py","hunks":["@@ -77,9 +77,11 @@ def _detect_openclaw_processes() -> list[str]:\n \n     # -- process scan ------------------------------------------------------\n     if sys.platform == \"win32\":\n+        from hermes_cli import _subprocess_compat\n+\n         try:\n             for exe in (\"openclaw.exe\", \"clawd.exe\"):\n-                result = subprocess.run(\n+                result = _subprocess_compat.run(\n                     [\"tasklist\", \"/FI\", f\"IMAGENAME eq {exe}\"],\n                     capture_output=True, text=True, timeout=5,\n                 )","@@ -93,7 +95,7 @@ def _detect_openclaw_processes() -> list[str]:\n                 'Where-Object { $_.CommandLine -match \"openclaw|clawd\" } | '\n                 'Select-Object -First 1 ProcessId'\n             )\n-            result = subprocess.run(\n+            result = _subprocess_compat.run(\n                 [\"powershell\", \"-NoProfile\", \"-Command\", ps_cmd],\n                 capture_output=True, text=True, timeout=5,\n             )"]},{"file":"hermes_cli/clipboard.py","hunks":["@@ -198,7 +198,9 @@ def _macos_osascript(dest: Path) -> bool:\n \n \n def _run_powershell(exe: str, script: str, timeout: int) -> subprocess.CompletedProcess:\n-    return subprocess.run(\n+    from hermes_cli import _subprocess_compat\n+\n+    return _subprocess_compat.run(\n         [exe, \"-NoProfile\", \"-NonInteractive\", \"-Command\", script],\n         capture_output=True, text=True, timeout=timeout,\n     )","@@ -254,9 +256,11 @@ def _powershell_save_image(exe: str, dest: Path, *, timeout: int, label: str) ->\n \n def _find_powershell() -> str | None:\n     \"\"\"Return the first available PowerShell executable, or None.\"\"\"\n+    from hermes_cli import _subprocess_compat\n+\n     for name in (\"powershell\", \"pwsh\"):\n         try:\n-            r = subprocess.run(\n+            r = _subprocess_compat.run(\n                 [name, \"-NoProfile\", \"-NonInteractive\", \"-Command\", \"echo ok\"],\n                 capture_output=True, text=True, timeout=5,\n             )"]},{"file":"hermes_cli/gateway.py","hunks":["@@ -380,6 +380,24 @@ def _matches_gateway_runtime(command: str) -> bool:\n \n     try:\n         if is_windows():\n+            try:\n+                import psutil  # type: ignore\n+\n+                for proc in psutil.process_iter([\"pid\", \"cmdline\"]):\n+                    pid = int(proc.info.get(\"pid\") or 0)\n+                    if pid == os.getpid() or pid in exclude_pids:\n+                        continue\n+                    command = \" \".join(proc.info.get(\"cmdline\") or [])\n+                    if _matches_gateway_runtime(command) and (\n+                        all_profiles or _matches_current_profile(command)\n+                    ):\n+                        _append_unique_pid(pids, pid, exclude_pids)\n+                return _filter_venv_launcher_stubs(pids) if len(pids) > 1 else pids\n+            except Exception:\n+                pass\n+\n+            from hermes_cli import _subprocess_compat\n+\n             # Prefer wmic when present (fast, stable output format).  On\n             # modern Windows 11 / Win 10 late builds, wmic has been\n             # removed as part of the WMIC deprecation — fall back to","@@ -390,7 +408,7 @@ def _matches_gateway_runtime(command: str) -> bool:\n             result = None\n             if wmic_path is not None:\n                 try:\n-                    result = subprocess.run(\n+                    result = _subprocess_compat.run(\n                         [\n                             wmic_path,\n                             \"process\",","@@ -421,7 +439,7 @@ def _matches_gateway_runtime(command: str) -> bool:\n                     \"}\"\n                 )\n                 try:\n-                    result = subprocess.run(\n+                    result = _subprocess_compat.run(\n                         [powershell, \"-NoProfile\", \"-Command\", ps_cmd],\n                         capture_output=True,\n                         text=True,","@@ -6632,7 +6650,6 @@ def _gateway_command_inner(args):\n             # path that can be reaped with the old gateway process.  If the\n             # Windows backend raises, intentionally preserve the existing\n             # generic failure fallback below.\n-            service_configured = gateway_windows.is_installed()\n             try:\n                 gateway_windows.restart()\n                 return"]},{"file":"hermes_cli/gateway_windows.py","hunks":["@@ -39,10 +39,10 @@\n from pathlib import Path\n from xml.sax.saxutils import escape\n \n+from hermes_cli import _subprocess_compat\n from hermes_cli._subprocess_compat import (\n     windows_detach_flags,\n     windows_detach_flags_without_breakaway,\n-    windows_hide_flags,\n )\n \n # Short timeouts: schtasks occasionally wedges and we don't want to hang forever.","@@ -157,7 +157,7 @@ def _exec_schtasks(args: list[str]) -> tuple[int, str, str]:\n     if schtasks is None:\n         return (1, \"\", \"schtasks.exe not found on PATH\")\n     try:\n-        proc = subprocess.run(\n+        proc = _subprocess_compat.run(\n             [schtasks, *args],\n             capture_output=True,\n             text=True,","@@ -168,10 +168,6 @@ def _exec_schtasks(args: list[str]) -> tuple[int, str, str]:\n             encoding=_schtasks_encoding(),\n             errors=\"replace\",\n             timeout=_SCHTASKS_TIMEOUT_S,\n-            # CREATE_NO_WINDOW avoids a flashing console window when the CLI\n-            # is itself hosted in a TUI. See tools/browser_tool.py for the\n-            # same pattern and the windows-subprocess-sigint-storm.md ref.\n-            creationflags=windows_hide_flags(),\n         )\n         return (proc.returncode, proc.stdout or \"\", proc.stderr or \"\")\n     except subprocess.TimeoutExpired:","@@ -1605,7 +1601,17 @@ def stop() -> None:\n         drained = _drain_gateway_pid(pid, _windows_stop_drain_timeout())\n \n     stopped_any = drained\n-    if is_task_registered():\n+    has_service_artifact = (\n+        get_task_script_path().exists()\n+        or get_task_script_path().with_suffix(\".vbs\").exists()\n+        or get_startup_entry_path().exists()\n+        or _legacy_startup_entry_path().exists()\n+    )\n+    if (\n+        has_service_artifact\n+        and os.getenv(\"HERMES_NONINTERACTIVE\") != \"1\"\n+        and is_task_registered()\n+    ):\n         code, _out, err = _exec_schtasks([\"/End\", \"/TN\", get_task_name()])\n         # schtasks returns nonzero when the task isn't currently running — don't treat that as an error.\n         if code == 0:","@@ -1673,7 +1679,8 @@ def restart() -> None:\n \n     # Give Windows a moment to release the listening port.\n     time.sleep(1.0)\n-    start()\n+    pid = _spawn_detached()\n+    _report_gateway_start(f\"direct spawn (PID {pid})\")\n \n     if not _wait_for_gateway_ready(timeout_s=15.0):\n         raise RuntimeError("]},{"file":"hermes_cli/main.py","hunks":["@@ -5730,6 +5730,8 @@ def _find_stale_dashboard_pids(\n \n     try:\n         if sys.platform == \"win32\":\n+            from hermes_cli import _subprocess_compat\n+\n             # wmic may emit text in the system code page (for example cp936\n             # on zh-CN systems), not UTF-8. In text mode, subprocess output\n             # decoding depends on Python's configuration (locale-dependent","@@ -5737,7 +5739,7 @@ def _find_stale_dashboard_pids(\n             # here is errors=\"ignore\": it prevents a reader-thread\n             # UnicodeDecodeError from leaving result.stdout=None and turning\n             # the later .split() into an AttributeError (#17049).\n-            result = subprocess.run(\n+            result = _subprocess_compat.run(\n                 [\"wmic\", \"process\", \"get\", \"ProcessId,CommandLine\", \"/FORMAT:LIST\"],\n                 capture_output=True,\n                 text=True,","@@ -5977,9 +5979,11 @@ def _kill_stale_dashboard_processes(\n     failed: list[tuple[int, str]] = []\n \n     if sys.platform == \"win32\":\n+        from hermes_cli import _subprocess_compat\n+\n         for pid in pids:\n             try:\n-                result = subprocess.run(\n+                result = _subprocess_compat.run(\n                     [\"taskkill\", \"/PID\", str(pid), \"/F\"],\n                     capture_output=True,\n                     text=True,"]},{"file":"hermes_cli/web_server.py","hunks":["@@ -2481,16 +2481,52 @@ def _record_completed_action(name: str, message: str, exit_code: int = 1) -> Non\n     _ACTION_RESULTS[name] = {\"exit_code\": exit_code, \"pid\": None}\n \n \n-def _dashboard_spawn_executable() -> str:\n-    \"\"\"Prefer pythonw.exe for detached dashboard actions on Windows.\"\"\"\n+def _dashboard_spawn_details() -> Tuple[str, Dict[str, str]]:\n+    \"\"\"Return (executable, env overlay) for detached dashboard actions.\n+\n+    On Windows this mirrors the gateway's uv-safe detached launcher logic so\n+    action spawns do not regress to console python.exe (which creates a visible\n+    terminal window). Non-Windows callsites get the current interpreter and no\n+    env overlay.\n+    \"\"\"\n     if sys.platform != \"win32\":\n-        return sys.executable\n+        return sys.executable, {}\n+\n     exe = sys.executable\n-    if exe.lower().endswith(\"python.exe\"):\n-        pythonw = os.path.join(os.path.dirname(exe), \"pythonw.exe\")\n-        if os.path.isfile(pythonw):\n-            return pythonw\n-    return exe\n+    try:\n+        from hermes_cli.gateway_windows import _resolve_detached_python\n+\n+        venv_root = os.environ.get(\"VIRTUAL_ENV\", \"\").strip()\n+        if not venv_root:\n+            for candidate in (PROJECT_ROOT / \"venv\", PROJECT_ROOT / \".venv\"):\n+                if (candidate / \"Scripts\" / \"python.exe\").exists():\n+                    venv_root = str(candidate)\n+                    break\n+        probe_exe = (\n+            os.path.join(venv_root, \"Scripts\", \"python.exe\")\n+            if venv_root\n+            else exe\n+        )\n+        windowless_exe, venv_dir, extra_pythonpath = _resolve_detached_python(probe_exe)\n+        env_overlay: Dict[str, str] = {}\n+        if venv_dir:\n+            env_overlay[\"VIRTUAL_ENV\"] = str(venv_dir)\n+            site_packages = Path(venv_dir) / \"Lib\" / \"site-packages\"\n+            if site_packages.exists() and str(site_packages) not in extra_pythonpath:\n+                extra_pythonpath = [*extra_pythonpath, str(site_packages)]\n+        if extra_pythonpath:\n+            existing = os.environ.get(\"PYTHONPATH\", \"\")\n+            env_overlay[\"PYTHONPATH\"] = os.pathsep.join(\n+                [*extra_pythonpath, existing] if existing else list(extra_pythonpath)\n+            )\n+        return windowless_exe, env_overlay\n+    except Exception:\n+        # Best-effort fallback: sibling pythonw keeps the legacy no-console path.\n+        if exe.lower().endswith(\"python.exe\"):\n+            pythonw = os.path.join(os.path.dirname(exe), \"pythonw.exe\")\n+            if os.path.isfile(pythonw):\n+                return pythonw, {}\n+    return exe, {}\n \n \n def _spawn_hermes_action(subcommand: List[str], name: str) -> subprocess.Popen:","@@ -2507,15 +2543,20 @@ def _spawn_hermes_action(subcommand: List[str], name: str) -> subprocess.Popen:\n         f\"\\n=== {name} started {time.strftime('%Y-%m-%d %H:%M:%S')} ===\\n\".encode()\n     )\n \n-    cmd = [_dashboard_spawn_executable(), \"-m\", \"hermes_cli.main\", *subcommand]\n+    spawn_executable, spawn_env_overlay = _dashboard_spawn_details()\n+    cmd = [spawn_executable, \"-m\", \"hermes_cli.main\", *subcommand]\n \n     popen_kwargs: Dict[str, Any] = {\n         \"cwd\": str(PROJECT_ROOT),\n         \"stdin\": subprocess.DEVNULL,\n         \"stdout\": log_file,\n         \"stderr\": subprocess.STDOUT,\n-        \"env\": {**os.environ, \"HERMES_NONINTERACTIVE\": \"1\"},\n+        \"env\": {**os.environ, \"HERMES_NONINTERACTIVE\": \"1\", **spawn_env_overlay},\n     }\n+    log_file.write(f\"spawn executable: {spawn_executable}\\n\".encode())\n+    if spawn_env_overlay:\n+        keys = \",\".join(sorted(spawn_env_overlay.keys()))\n+        log_file.write(f\"spawn env overlay keys: {keys}\\n\".encode())\n     if sys.platform == \"win32\":\n         popen_kwargs[\"creationflags\"] = windows_detach_flags()\n     else:","@@ -13547,6 +13588,15 @@ def start_server(\n     — used when a profile alias (``<profile> dashboard``) routes to the\n     machine dashboard.\n     \"\"\"\n+    # Desktop spawns this backend via a no-console venv python; a uv\n+    # pythonw→python re-exec can still auto-allocate a console. Drop it.\n+    # No-op on POSIX and when launched from an interactive shell.\n+    try:\n+        import hermes_bootstrap\n+        hermes_bootstrap.detach_orphan_console()\n+    except Exception:\n+        pass\n+\n     import uvicorn\n \n     try:"]},{"file":"hermes_constants.py","hunks":["@@ -561,12 +561,15 @@ def agent_browser_runnable(path: str | None) -> bool:\n         return False\n     import subprocess\n \n+    from hermes_cli import _subprocess_compat\n+\n     try:\n-        result = subprocess.run(\n+        result = _subprocess_compat.run(\n             [path, \"--version\"],\n             capture_output=True,\n             timeout=10,\n             env=with_hermes_node_path(),\n+            stdin=subprocess.DEVNULL,\n         )\n     except (OSError, subprocess.TimeoutExpired, ValueError):\n         return False"]},{"file":"plugins/platforms/discord/adapter.py","hunks":["@@ -665,7 +665,9 @@ def pcm_to_wav(pcm_data: bytes, output_path: str,\n             f.write(pcm_data)\n             pcm_path = f.name\n         try:\n-            subprocess.run(\n+            from hermes_cli import _subprocess_compat\n+\n+            _subprocess_compat.run(\n                 [\n                     \"ffmpeg\", \"-y\", \"-loglevel\", \"error\",\n                     \"-f\", \"s16le\",","@@ -679,7 +681,6 @@ def pcm_to_wav(pcm_data: bytes, output_path: str,\n                 check=True,\n                 timeout=10,\n                 stdin=subprocess.DEVNULL,\n-                creationflags=windows_hide_flags(),\n             )\n         finally:\n             try:"]},{"file":"plugins/platforms/whatsapp/adapter.py","hunks":["@@ -78,8 +78,10 @@ def _kill_port_process(port: int) -> None:\n     \"\"\"Kill any process *listening* on the given TCP port (a stale bridge).\"\"\"\n     try:\n         if _IS_WINDOWS:\n+            from hermes_cli import _subprocess_compat\n+\n             # Use netstat to find the PID bound to this port, then taskkill\n-            result = subprocess.run(\n+            result = _subprocess_compat.run(\n                 [\"netstat\", \"-ano\", \"-p\", \"TCP\"],\n                 capture_output=True, text=True, timeout=5,\n             )","@@ -89,7 +91,7 @@ def _kill_port_process(port: int) -> None:\n                     local_addr = parts[1]\n                     if local_addr.endswith(f\":{port}\"):\n                         try:\n-                            subprocess.run(\n+                            _subprocess_compat.run(\n                                 [\"taskkill\", \"/PID\", parts[4], \"/F\"],\n                                 capture_output=True, timeout=5,\n                             )","@@ -207,11 +209,13 @@ def _write_bridge_pidfile(session_path: Path, pid: int) -> None:\n def _terminate_bridge_process(proc, *, force: bool = False) -> None:\n     \"\"\"Terminate the bridge process using process-tree semantics where possible.\"\"\"\n     if _IS_WINDOWS:\n+        from hermes_cli import _subprocess_compat\n+\n         cmd = [\"taskkill\", \"/PID\", str(proc.pid), \"/T\"]\n         if force:\n             cmd.append(\"/F\")\n         try:\n-            result = subprocess.run(\n+            result = _subprocess_compat.run(\n                 cmd,\n                 capture_output=True,\n                 text=True,"]},{"file":"tests/gateway/test_status.py","hunks":["@@ -618,20 +618,25 @@ def no_proc(self, *args, **kwargs):\n \n class TestTerminatePid:\n     def test_force_uses_taskkill_on_windows(self, monkeypatch):\n+        from hermes_cli import _subprocess_compat\n+\n         calls = []\n         monkeypatch.setattr(status, \"_IS_WINDOWS\", True)\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n \n-        def fake_run(cmd, capture_output=False, text=False, timeout=None):\n-            calls.append((cmd, capture_output, text, timeout))\n+        def fake_run(cmd, capture_output=False, text=False, timeout=None, **kwargs):\n+            calls.append((cmd, capture_output, text, timeout, kwargs))\n             return SimpleNamespace(returncode=0, stdout=\"\", stderr=\"\")\n \n         monkeypatch.setattr(status.subprocess, \"run\", fake_run)\n \n         status.terminate_pid(123, force=True)\n \n-        assert calls == [\n-            ([\"taskkill\", \"/PID\", \"123\", \"/T\", \"/F\"], True, True, 10)\n-        ]\n+        assert len(calls) == 1\n+        cmd, capture_output, text, timeout, kwargs = calls[0]\n+        assert cmd == [\"taskkill\", \"/PID\", \"123\", \"/T\", \"/F\"]\n+        assert (capture_output, text, timeout) == (True, True, 10)\n+        assert kwargs[\"creationflags\"] & 0x08000000\n \n     def test_force_falls_back_to_sigterm_when_taskkill_missing(self, monkeypatch):\n         calls = []"]},{"file":"tests/hermes_cli/test_claw.py","hunks":["@@ -725,8 +725,9 @@ def test_detects_systemd_service(self):\n     def test_returns_match_on_windows_when_openclaw_exe_running(self):\n         with patch.object(claw_mod, \"sys\") as mock_sys:\n             mock_sys.platform = \"win32\"\n-            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n-                mock_subprocess.run.side_effect = [\n+            # Windows scans go through the hidden-spawn primitive (no console flash).\n+            with patch(\"hermes_cli._subprocess_compat.run\") as mock_run:\n+                mock_run.side_effect = [\n                     MagicMock(returncode=0, stdout=\"openclaw.exe                 1234 Console    1     45,056 K\\n\"),\n                 ]\n                 result = claw_mod._detect_openclaw_processes()","@@ -736,8 +737,8 @@ def test_returns_match_on_windows_when_openclaw_exe_running(self):\n     def test_returns_match_on_windows_when_node_exe_has_openclaw_in_cmdline(self):\n         with patch.object(claw_mod, \"sys\") as mock_sys:\n             mock_sys.platform = \"win32\"\n-            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n-                mock_subprocess.run.side_effect = [\n+            with patch(\"hermes_cli._subprocess_compat.run\") as mock_run:\n+                mock_run.side_effect = [\n                     MagicMock(returncode=0, stdout=\"\"),  # tasklist openclaw.exe\n                     MagicMock(returncode=0, stdout=\"\"),  # tasklist clawd.exe\n                     MagicMock(returncode=0, stdout=\"1234\\n\"),  # PowerShell","@@ -749,8 +750,8 @@ def test_returns_match_on_windows_when_node_exe_has_openclaw_in_cmdline(self):\n     def test_returns_empty_on_windows_when_nothing_found(self):\n         with patch.object(claw_mod, \"sys\") as mock_sys:\n             mock_sys.platform = \"win32\"\n-            with patch.object(claw_mod, \"subprocess\") as mock_subprocess:\n-                mock_subprocess.run.side_effect = [\n+            with patch(\"hermes_cli._subprocess_compat.run\") as mock_run:\n+                mock_run.side_effect = [\n                     MagicMock(returncode=0, stdout=\"\"),\n                     MagicMock(returncode=0, stdout=\"\"),\n                     MagicMock(returncode=0, stdout=\"\"),"]},{"file":"tests/hermes_cli/test_gateway.py","hunks":["@@ -887,23 +887,20 @@ def test_reap_unsupervised_orphans_returns_false_when_none_found(monkeypatch):\n \n \n def test_scan_gateway_pids_detects_windows_hermes_exe_case_variants(monkeypatch):\n+    # Windows scan now goes through psutil first (no console spawn). A\n+    # uppercase ``Hermes.EXE gateway run`` must still match case-insensitively.\n+    import psutil\n+\n     monkeypatch.setattr(gateway, \"is_windows\", lambda: True)\n     monkeypatch.setattr(gateway, \"_get_ancestor_pids\", lambda: set())\n-    monkeypatch.setattr(gateway.shutil, \"which\", lambda name: \"wmic.exe\" if name == \"wmic\" else None)\n-\n-    def fake_run(cmd, **kwargs):\n-        if cmd[:4] == [\"wmic.exe\", \"process\", \"get\", \"ProcessId,CommandLine\"]:\n-            return SimpleNamespace(\n-                returncode=0,\n-                stdout=(\n-                    \"CommandLine=C:\\\\Program Files\\\\Hermes\\\\Hermes.EXE gateway run --replace\\n\"\n-                    \"ProcessId=2468\\n\\n\"\n-                ),\n-                stderr=\"\",\n-            )\n-        raise AssertionError(f\"Unexpected command: {cmd}\")\n \n-    monkeypatch.setattr(gateway.subprocess, \"run\", fake_run)\n+    proc = SimpleNamespace(\n+        info={\n+            \"pid\": 2468,\n+            \"cmdline\": [\"C:\\\\Program Files\\\\Hermes\\\\Hermes.EXE\", \"gateway\", \"run\", \"--replace\"],\n+        }\n+    )\n+    monkeypatch.setattr(psutil, \"process_iter\", lambda attrs=None: [proc])\n \n     assert gateway._scan_gateway_pids(set(), all_profiles=True) == [2468]\n "]},{"file":"tests/hermes_cli/test_gateway_windows.py","hunks":["@@ -29,6 +29,31 @@ def test_schtasks_fallback_does_not_hide_unknown_errors():\n     assert gateway_windows._should_fall_back(1, \"ERROR: The system cannot find the file specified.\") is False\n \n \n+def test_noninteractive_stop_skips_schtasks_query(monkeypatch, tmp_path):\n+    \"\"\"Desktop-triggered restarts must not invoke schtasks.exe.\n+\n+    schtasks is a console-subsystem binary; on Windows Terminal default hosts it\n+    can visibly pop a terminal even for `/Query`. Noninteractive desktop actions\n+    already stop the known gateway PID directly, so service-manager probing is\n+    unnecessary.\n+    \"\"\"\n+\n+    script = tmp_path / \"Hermes_Gateway.cmd\"\n+    script.write_text(\"\", encoding=\"utf-8\")\n+\n+    monkeypatch.setenv(\"HERMES_NONINTERACTIVE\", \"1\")\n+    monkeypatch.setattr(gateway_windows, \"_assert_windows\", lambda: None)\n+    monkeypatch.setattr(gateway_windows, \"get_task_script_path\", lambda: script)\n+    monkeypatch.setattr(gateway_windows, \"get_startup_entry_path\", lambda: tmp_path / \"Hermes_Gateway.vbs\")\n+    monkeypatch.setattr(gateway_windows, \"_legacy_startup_entry_path\", lambda: tmp_path / \"Hermes_Gateway.cmd\")\n+    monkeypatch.setattr(gateway_windows, \"is_task_registered\", lambda: pytest.fail(\"must not call schtasks\"))\n+    monkeypatch.setattr(\"gateway.status.get_running_pid\", lambda: None)\n+    monkeypatch.setattr(gateway_windows, \"_collect_gateway_stop_pids\", lambda pid=None: [])\n+    monkeypatch.setattr(gateway_windows, \"_force_terminate_known_gateway_pids\", lambda pids: 0)\n+\n+    gateway_windows.stop()\n+\n+\n def test_schtasks_encoding_falls_back_to_utf8(monkeypatch):\n     \"\"\"A broken/empty locale must not leave us without a decoder (issue #38172).\"\"\"\n ","@@ -112,6 +137,23 @@ def test_build_gateway_argv_uses_base_pythonw_for_uv_venv_launcher(monkeypatch,\n     assert str(site_packages) in env_overlay[\"PYTHONPATH\"].split(gateway_windows.os.pathsep)\n \n \n+def test_restart_relaunches_directly_without_start_service_probe(monkeypatch):\n+    calls = []\n+\n+    monkeypatch.setattr(gateway_windows, \"_assert_windows\", lambda: None)\n+    monkeypatch.setattr(gateway_windows, \"stop\", lambda: calls.append(\"stop\"))\n+    monkeypatch.setattr(gateway_windows, \"_wait_for_gateway_absent\", lambda *a, **k: True)\n+    monkeypatch.setattr(gateway_windows.time, \"sleep\", lambda *_a, **_k: None)\n+    monkeypatch.setattr(gateway_windows, \"_spawn_detached\", lambda: 4321)\n+    monkeypatch.setattr(gateway_windows, \"_report_gateway_start\", lambda via: calls.append((\"report\", via)))\n+    monkeypatch.setattr(gateway_windows, \"_wait_for_gateway_ready\", lambda *a, **k: [4321])\n+    monkeypatch.setattr(gateway_windows, \"start\", lambda: pytest.fail(\"restart must not call start()\"))\n+\n+    gateway_windows.restart()\n+\n+    assert calls == [\"stop\", (\"report\", \"direct spawn (PID 4321)\")]\n+\n+\n class TestStableWindowsGatewayWorkingDir:\n     def test_stable_gateway_working_dir_uses_hermes_home(self, tmp_path, monkeypatch):\n         home = tmp_path / \".hermes\""]},{"file":"tests/hermes_cli/test_web_server.py","hunks":["@@ -215,6 +215,75 @@ def test_falls_back_to_random_token(self, monkeypatch):\n         assert ws._SESSION_TOKEN and len(ws._SESSION_TOKEN) >= 32\n \n \n+class TestDashboardActionSpawnDetails:\n+    def test_windows_uses_uv_safe_pythonw_with_env_overlay(self, monkeypatch):\n+        import hermes_cli.web_server as ws\n+\n+        monkeypatch.setattr(ws.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(ws.sys, \"executable\", r\"C:\\venv\\Scripts\\python.exe\")\n+        monkeypatch.setenv(\"PYTHONPATH\", r\"C:\\existing\")\n+        monkeypatch.setattr(\n+            \"hermes_cli.gateway_windows._resolve_detached_python\",\n+            lambda _exe: (\n+                r\"C:\\base\\pythonw.exe\",\n+                Path(r\"C:\\venv\"),\n+                [r\"C:\\venv\\Lib\\site-packages\"],\n+            ),\n+        )\n+\n+        executable, env_overlay = ws._dashboard_spawn_details()\n+\n+        assert executable == r\"C:\\base\\pythonw.exe\"\n+        assert env_overlay[\"VIRTUAL_ENV\"] == str(Path(r\"C:\\venv\"))\n+        assert env_overlay[\"PYTHONPATH\"] == os.pathsep.join(\n+            [r\"C:\\venv\\Lib\\site-packages\", r\"C:\\existing\"]\n+        )\n+\n+    def test_windows_falls_back_to_sibling_pythonw_when_resolver_fails(self, monkeypatch):\n+        import hermes_cli.web_server as ws\n+\n+        exe = \"C:/venv/Scripts/python.exe\"\n+        expected = \"C:/venv/Scripts/pythonw.exe\"\n+\n+        monkeypatch.setattr(ws.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(ws.sys, \"executable\", exe)\n+        monkeypatch.setattr(\n+            \"hermes_cli.gateway_windows._resolve_detached_python\",\n+            lambda _exe: (_ for _ in ()).throw(RuntimeError(\"boom\")),\n+        )\n+        monkeypatch.setattr(ws.os.path, \"isfile\", lambda candidate: candidate == expected)\n+\n+        executable, env_overlay = ws._dashboard_spawn_details()\n+\n+        assert executable == expected\n+        assert env_overlay == {}\n+\n+    def test_windows_resolves_venv_from_project_when_virtual_env_missing(self, monkeypatch, tmp_path):\n+        import hermes_cli.web_server as ws\n+\n+        project = tmp_path / \"project\"\n+        scripts = project / \"venv\" / \"Scripts\"\n+        site_packages = project / \"venv\" / \"Lib\" / \"site-packages\"\n+        scripts.mkdir(parents=True)\n+        site_packages.mkdir(parents=True)\n+        (scripts / \"python.exe\").write_text(\"\", encoding=\"utf-8\")\n+\n+        monkeypatch.delenv(\"VIRTUAL_ENV\", raising=False)\n+        monkeypatch.setattr(ws.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(ws.sys, \"executable\", r\"C:\\base\\pythonw.exe\")\n+        monkeypatch.setattr(ws, \"PROJECT_ROOT\", project)\n+        monkeypatch.setattr(\n+            \"hermes_cli.gateway_windows._resolve_detached_python\",\n+            lambda exe: (r\"C:\\base\\pythonw.exe\", project / \"venv\", []),\n+        )\n+\n+        executable, env_overlay = ws._dashboard_spawn_details()\n+\n+        assert executable == r\"C:\\base\\pythonw.exe\"\n+        assert env_overlay[\"VIRTUAL_ENV\"] == str(project / \"venv\")\n+        assert str(site_packages) in env_overlay[\"PYTHONPATH\"].split(os.pathsep)\n+\n+\n # ---------------------------------------------------------------------------\n # web_server tests (FastAPI endpoints)\n # ---------------------------------------------------------------------------"]},{"file":"tests/test_hermes_bootstrap.py","hunks":["@@ -21,6 +21,7 @@\n \n import io\n import os\n+import platform\n import subprocess\n import sys\n import textwrap","@@ -231,6 +232,82 @@ def reconfigure(self, **kwargs):\n         hb.apply_windows_utf8_bootstrap()\n \n \n+class TestWindowsPlatformProbeGuard:\n+    def test_windows_bootstrap_disables_platform_syscmd_subprocess(self):\n+        hb = _fresh_import()\n+        hb._IS_WINDOWS = True\n+        hb._bootstrap_applied = False\n+\n+        original = getattr(platform, \"_syscmd_ver\", None)\n+        try:\n+            hb.apply_windows_utf8_bootstrap()\n+\n+            assert platform._syscmd_ver(\"Windows\", \"\", \"\") == (\"Windows\", \"\", \"\")\n+        finally:\n+            if original is not None:\n+                platform._syscmd_ver = original\n+\n+\n+class TestDetachOrphanConsole:\n+    \"\"\"detach_orphan_console() frees a solo-owned console (the uv pythonw→python\n+    phantom) but leaves a shared interactive console attached, and is a pure\n+    no-op on POSIX. It is intentionally NOT run at import time.\"\"\"\n+\n+    def test_noop_on_posix(self):\n+        hb = _fresh_import()\n+        hb._IS_WINDOWS = False\n+        assert hb.detach_orphan_console() is False\n+\n+    def test_not_called_at_import_time(self):\n+        # The FreeConsole catch-all must be opt-in per background entry point,\n+        # never an import side effect (would detach the interactive CLI/TUI).\n+        import pathlib\n+        src = pathlib.Path(_fresh_import().__file__).read_text(encoding=\"utf-8\")\n+        body = src.split(\"def detach_orphan_console\")[0]\n+        assert \"FreeConsole\" not in body, (\n+            \"FreeConsole must live only inside detach_orphan_console(), not in \"\n+            \"apply_windows_utf8_bootstrap() / module import path\"\n+        )\n+\n+    def _fake_ctypes(self, monkeypatch, window, nproc):\n+        import ctypes\n+\n+        class _K:\n+            def __init__(self):\n+                self.freed = False\n+            def GetConsoleWindow(self):\n+                return window\n+            def GetConsoleProcessList(self, buf, n):\n+                return nproc\n+            def FreeConsole(self):\n+                self.freed = True\n+\n+        k = _K()\n+        monkeypatch.setattr(ctypes, \"windll\", type(\"_W\", (), {\"kernel32\": k})(), raising=False)\n+        return k\n+\n+    def test_frees_when_solo_owner(self, monkeypatch):\n+        hb = _fresh_import()\n+        hb._IS_WINDOWS = True\n+        k = self._fake_ctypes(monkeypatch, window=1, nproc=1)\n+        assert hb.detach_orphan_console() is True\n+        assert k.freed is True\n+\n+    def test_leaves_shared_console_attached(self, monkeypatch):\n+        hb = _fresh_import()\n+        hb._IS_WINDOWS = True\n+        k = self._fake_ctypes(monkeypatch, window=1, nproc=2)\n+        assert hb.detach_orphan_console() is False\n+        assert k.freed is False\n+\n+    def test_noop_without_console(self, monkeypatch):\n+        hb = _fresh_import()\n+        hb._IS_WINDOWS = True\n+        k = self._fake_ctypes(monkeypatch, window=0, nproc=1)\n+        assert hb.detach_orphan_console() is False\n+        assert k.freed is False\n+\n+\n class TestEntryPointsImportBootstrap:\n     \"\"\"Every Hermes entry point must import hermes_bootstrap as its\n     first non-docstring import.  We check this by scanning source files"]},{"file":"tests/test_hermes_constants.py","hunks":["@@ -1,6 +1,8 @@\n \"\"\"Tests for hermes_constants module.\"\"\"\n \n import os\n+import subprocess\n+import sys\n from pathlib import Path\n \n import pytest","@@ -612,6 +614,30 @@ def test_npx_fallback_form_accepted(self):\n         assert agent_browser_runnable(\"/usr/local/bin/npx agent-browser\") is True\n \n \n+class TestAgentBrowserRunnableWindows:\n+    def test_windows_validation_hides_console_subprocess(self, tmp_path, monkeypatch):\n+        from hermes_cli import _subprocess_compat\n+\n+        exe = tmp_path / \"agent-browser-win32-x64.exe\"\n+        exe.write_text(\"\", encoding=\"utf-8\")\n+        exe.chmod(0o755)\n+        captured = {}\n+\n+        def fake_run(args, **kwargs):\n+            captured[\"args\"] = args\n+            captured.update(kwargs)\n+            return type(\"Result\", (), {\"returncode\": 0})()\n+\n+        monkeypatch.setattr(sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+        monkeypatch.setattr(subprocess, \"run\", fake_run)\n+\n+        assert agent_browser_runnable(str(exe)) is True\n+        assert captured[\"args\"] == [str(exe), \"--version\"]\n+        assert \"creationflags\" in captured\n+        assert captured[\"stdin\"] is subprocess.DEVNULL\n+\n+\n class TestGetHermesDir:\n     \"\"\"Tests for ``get_hermes_dir(new_subpath, old_name)``.\n "]},{"file":"tests/test_no_visible_console_spawns.py","hunks":["@@ -0,0 +1,121 @@\n+\"\"\"Enforcement for the \"no visible terminal on Windows\" invariant.\n+\n+Windows console-subsystem programs (``taskkill``, ``schtasks``, ``agent-browser``,\n+``git-bash`` …) pop a console window unless spawned with ``CREATE_NO_WINDOW``.\n+Relying on each call site to remember the flag is how cron-driven and future\n+spawns leaked terminal windows. The durable fix is a single chokepoint —\n+``hermes_cli._subprocess_compat.run`` / ``.popen`` — that always injects the\n+flag on Windows, plus the ``FreeConsole`` catch-all in ``hermes_bootstrap`` for\n+Python children.\n+\n+These tests pin both halves of that contract:\n+\n+1. The primitive actually injects ``CREATE_NO_WINDOW`` (and merges, so detach\n+   callers still work).\n+2. No source file spawns a known console exe with a *raw* ``subprocess`` call,\n+   which would bypass the primitive and reintroduce the window.\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+import re\n+from pathlib import Path\n+\n+import pytest\n+\n+from hermes_cli import _subprocess_compat\n+\n+REPO_ROOT = Path(__file__).resolve().parent.parent\n+_CREATE_NO_WINDOW = 0x08000000\n+\n+\n+class TestPrimitiveInjectsNoWindow:\n+    def test_run_injects_create_no_window_on_windows(self, monkeypatch):\n+        captured: dict = {}\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+        monkeypatch.setattr(\n+            _subprocess_compat.subprocess, \"run\", lambda cmd, **kw: captured.update(kw) or \"ok\"\n+        )\n+\n+        _subprocess_compat.run([\"taskkill\"], timeout=5)\n+\n+        assert captured[\"creationflags\"] & _CREATE_NO_WINDOW\n+\n+    def test_popen_injects_create_no_window_on_windows(self, monkeypatch):\n+        captured: dict = {}\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+        monkeypatch.setattr(\n+            _subprocess_compat.subprocess, \"Popen\", lambda cmd, **kw: captured.update(kw) or \"ok\"\n+        )\n+\n+        _subprocess_compat.popen([\"agent-browser\"])\n+\n+        assert captured[\"creationflags\"] & _CREATE_NO_WINDOW\n+\n+    def test_merges_with_existing_detach_flags(self, monkeypatch):\n+        captured: dict = {}\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+        monkeypatch.setattr(\n+            _subprocess_compat.subprocess, \"run\", lambda cmd, **kw: captured.update(kw) or \"ok\"\n+        )\n+\n+        detach = _subprocess_compat.windows_detach_flags()\n+        _subprocess_compat.run([\"x\"], creationflags=detach)\n+\n+        assert captured[\"creationflags\"] & _CREATE_NO_WINDOW\n+        assert captured[\"creationflags\"] & detach == detach\n+\n+    def test_no_op_on_posix(self, monkeypatch):\n+        captured: dict = {}\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", False)\n+        monkeypatch.setattr(\n+            _subprocess_compat.subprocess, \"run\", lambda cmd, **kw: captured.update(kw) or \"ok\"\n+        )\n+\n+        _subprocess_compat.run([\"x\"])\n+\n+        assert \"creationflags\" not in captured\n+\n+\n+# Windows-only console tools — they have no POSIX use, so a raw ``subprocess``\n+# spawn is unambiguously a Windows path that flashes a terminal. Banning them\n+# repo-wide is a pure win (cross-platform tools like git/ffmpeg/node are NOT\n+# listed: they have legitimate foreground/POSIX uses a blanket ban would break;\n+# their Windows-background call sites are routed through the primitive instead).\n+# ``_subprocess_compat.run/.popen`` calls never match these (different prefix).\n+_WINDOWS_ONLY_CONSOLE_EXES = (\"taskkill\", \"schtasks\", \"wmic\", \"netstat\", \"tasklist\")\n+_RAW_CONSOLE_SPAWNS = [\n+    re.compile(rf\"\"\"subprocess\\.(?:run|Popen|call)\\(\\s*\\[\\s*[\"']{exe}[\"']\"\"\")\n+    for exe in _WINDOWS_ONLY_CONSOLE_EXES\n+]\n+\n+# The primitive itself is allowed to call raw subprocess — it IS the chokepoint.\n+_ALLOWED = {REPO_ROOT / \"hermes_cli\" / \"_subprocess_compat.py\"}\n+\n+\n+# Dev/CI tooling that never ships to a user's Windows desktop, where a flashing\n+# console is irrelevant and importing hermes_cli would be inappropriate.\n+_SKIP_DIRS = {\"tests\", \"node_modules\", \".venv\", \"venv\", \"scripts\"}\n+\n+\n+def _python_sources():\n+    for path in REPO_ROOT.rglob(\"*.py\"):\n+        if _SKIP_DIRS & set(path.parts):\n+            continue\n+        if path in _ALLOWED:\n+            continue\n+        yield path\n+\n+\n[redacted:email](\"pattern\", _RAW_CONSOLE_SPAWNS, ids=_WINDOWS_ONLY_CONSOLE_EXES)\n+def test_no_raw_console_exe_spawns(pattern):\n+    offenders = [\n+        str(path.relative_to(REPO_ROOT))\n+        for path in _python_sources()\n+        if pattern.search(path.read_text(encoding=\"utf-8\", errors=\"ignore\"))\n+    ]\n+\n+    assert not offenders, (\n+        \"Console-subsystem exe spawned via raw subprocess (flashes a terminal on \"\n+        f\"Windows). Route through hermes_cli._subprocess_compat.run/.popen instead: {offenders}\"\n+    )"]},{"file":"tests/tools/test_browser_hardening.py","hunks":["@@ -89,6 +89,28 @@ def mock_exists(self):\n         with pytest.raises(FileNotFoundError, match=\"cached\"):\n             bt._find_agent_browser()\n \n+    def test_windows_prefers_native_agent_browser_exe_over_cmd_shim(self, tmp_path, monkeypatch):\n+        import tools.browser_tool as bt\n+\n+        repo = tmp_path / \"repo\"\n+        native = repo / \"node_modules\" / \"agent-browser\" / \"bin\" / \"agent-browser-win32-x64.exe\"\n+        cmd = repo / \"node_modules\" / \".bin\" / \"agent-browser.cmd\"\n+        native.parent.mkdir(parents=True)\n+        cmd.parent.mkdir(parents=True)\n+        native.write_text(\"\", encoding=\"utf-8\")\n+        cmd.write_text(\"\", encoding=\"utf-8\")\n+\n+        def fake_which(command, path=None):\n+            return str(cmd) if path == str(cmd.parent) else None\n+\n+        monkeypatch.setattr(bt.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(bt.shutil, \"which\", fake_which)\n+        monkeypatch.setattr(bt, \"agent_browser_runnable\", lambda path: True)\n+        monkeypatch.setattr(bt, \"_merge_browser_path\", lambda path: \"\")\n+        monkeypatch.setattr(bt, \"__file__\", str(repo / \"tools\" / \"browser_tool.py\"))\n+\n+        assert bt._find_agent_browser() == str(native)\n+\n \n # ---------------------------------------------------------------------------\n # Caching: _get_command_timeout"]},{"file":"tests/tools/test_windows_native_support.py","hunks":["@@ -188,11 +188,13 @@ class TestTerminatePidRoutingOnWindows:\n \n     def test_force_uses_taskkill_on_windows(self, monkeypatch):\n         from gateway import status\n+        from hermes_cli import _subprocess_compat\n \n         captured = {}\n \n         def fake_run(args, **kwargs):\n             captured[\"args\"] = args\n+            captured.update(kwargs)\n             result = MagicMock()\n             result.returncode = 0\n             result.stderr = \"\"","@@ -200,6 +202,7 @@ def fake_run(args, **kwargs):\n             return result\n \n         monkeypatch.setattr(status, \"_IS_WINDOWS\", True)\n+        monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n         monkeypatch.setattr(status.subprocess, \"run\", fake_run)\n         status.terminate_pid(12345, force=True)\n ","@@ -208,6 +211,7 @@ def fake_run(args, **kwargs):\n         assert \"12345\" in captured[\"args\"]\n         assert \"/T\" in captured[\"args\"]\n         assert \"/F\" in captured[\"args\"]\n+        assert captured[\"creationflags\"] & 0x08000000\n \n     def test_force_taskkill_failure_raises_oserror(self, monkeypatch):\n         from gateway import status"]},{"file":"tools/browser_tool.py","hunks":["@@ -68,7 +68,7 @@\n from hermes_constants import agent_browser_runnable, get_hermes_home\n from utils import env_int, is_truthy_value\n from hermes_cli.config import DEFAULT_CONFIG, cfg_get\n-from hermes_cli._subprocess_compat import windows_hide_flags\n+from hermes_cli import _subprocess_compat\n \n try:\n     from tools.website_policy import check_website_access","@@ -905,23 +905,19 @@ def _run_tmp(cmd: str, cmd_args: List[str]) -> Dict[str, Any]:\n             #   fileno=1 (stderr dup'd onto stdout at the OS level).\n             # * close_fds=True → block inheritance of every other handle.\n             #   (Default on POSIX; must be explicit on Windows for stdio.)\n+            # CREATE_NO_WINDOW is applied by _subprocess_compat.popen. We do NOT\n+            # add CREATE_NEW_PROCESS_GROUP: on Python 3.11 Windows it interacts\n+            # with asyncio's ProactorEventLoop such that the subprocess creation\n+            # cancels the running loop task, surfacing as KeyboardInterrupt in\n+            # app.run() and tearing down the CLI mid-turn (diag:\n+            # \"asyncio.CancelledError → KeyboardInterrupt\").\n             _popen_extra: dict = {}\n             if os.name == \"nt\":\n-                # CREATE_NO_WINDOW → don't attach a console (cmd.exe would\n-                # otherwise briefly allocate one for the .cmd shim).\n-                # Do NOT add CREATE_NEW_PROCESS_GROUP: on Python 3.11 Windows\n-                # it interacts with asyncio's ProactorEventLoop such that the\n-                # subprocess creation cancels the running loop task, which\n-                # surfaces as KeyboardInterrupt in app.run() and tears down\n-                # the CLI mid-turn. The agent thread's subprocess spawn\n-                # unwound MainThread's prompt_toolkit loop that way — see\n-                # diag log: \"asyncio.CancelledError → KeyboardInterrupt\".\n-                _popen_extra[\"creationflags\"] = windows_hide_flags()\n                 _popen_extra[\"close_fds\"] = True\n                 _si = subprocess.STARTUPINFO()\n                 _si.dwFlags |= subprocess.STARTF_USESTDHANDLES\n                 _popen_extra[\"startupinfo\"] = _si\n-            proc = subprocess.Popen(\n+            proc = _subprocess_compat.popen(\n                 full, stdout=stdout_fd, stderr=stderr_fd,\n                 stdin=subprocess.DEVNULL, env=browser_env,\n                 **_popen_extra,","@@ -1942,6 +1938,12 @@ def _find_agent_browser() -> str:\n     repo_root = Path(__file__).parent.parent\n     local_bin_dir = repo_root / \"node_modules\" / \".bin\"\n     if local_bin_dir.is_dir():\n+        if sys.platform == \"win32\":\n+            native = repo_root / \"node_modules\" / \"agent-browser\" / \"bin\" / \"agent-browser-win32-x64.exe\"\n+            if native.exists() and agent_browser_runnable(str(native)):\n+                _cached_agent_browser = str(native)\n+                _agent_browser_resolved = True\n+                return _cached_agent_browser\n         local_which = shutil.which(\"agent-browser\", path=str(local_bin_dir))\n         if local_which and agent_browser_runnable(local_which):\n             _cached_agent_browser = local_which","@@ -2191,17 +2193,16 @@ def _run_browser_command(\n             # three explicit handles (no leaked parent-console handles to\n             # confuse the Rust binary's daemon-spawn), and close_fds=True to\n             # block inheritance of everything else.\n+            # CREATE_NO_WINDOW via _subprocess_compat.popen; NO\n+            # CREATE_NEW_PROCESS_GROUP (cancels asyncio loop task on Python 3.11\n+            # Windows → KeyboardInterrupt in CLI MainThread).\n             _popen_extra: dict = {}\n             if os.name == \"nt\":\n-                # See matching block at the other Popen site — CREATE_NO_WINDOW\n-                # only, NO CREATE_NEW_PROCESS_GROUP (cancels asyncio loop task\n-                # on Python 3.11 Windows → KeyboardInterrupt in CLI MainThread).\n-                _popen_extra[\"creationflags\"] = windows_hide_flags()\n                 _popen_extra[\"close_fds\"] = True\n                 _si = subprocess.STARTUPINFO()\n                 _si.dwFlags |= subprocess.STARTF_USESTDHANDLES\n                 _popen_extra[\"startupinfo\"] = _si\n-            proc = subprocess.Popen(\n+            proc = _subprocess_compat.popen(\n                 cmd_parts,\n                 stdout=stdout_fd,\n                 stderr=stderr_fd,"]},{"file":"tools/environments/local.py","hunks":["@@ -13,7 +13,7 @@\n from pathlib import Path\n \n from tools.environments.base import BaseEnvironment, _pipe_stdin\n-from hermes_cli._subprocess_compat import windows_hide_flags\n+from hermes_cli import _subprocess_compat\n \n _IS_WINDOWS = platform.system() == \"Windows\"\n ","@@ -738,9 +738,7 @@ def _run_bash(self, cmd_string: str, *, login: bool = False,\n \n         _popen_cwd = self.cwd\n \n-        _popen_kwargs = {\"creationflags\": windows_hide_flags()} if _IS_WINDOWS else {}\n-\n-        proc = subprocess.Popen(\n+        proc = _subprocess_compat.popen(\n             args,\n             text=True,\n             env=run_env,","@@ -751,7 +749,6 @@ def _run_bash(self, cmd_string: str, *, login: bool = False,\n             stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL,\n             start_new_session=True,\n             cwd=_popen_cwd,\n-            **_popen_kwargs,\n         )\n         if not _IS_WINDOWS:\n             try:"]},{"file":"tools/process_registry.py","hunks":["@@ -42,6 +42,7 @@\n \n _IS_WINDOWS = platform.system() == \"Windows\"\n from tools.environments.local import _find_shell, _resolve_safe_cwd, _sanitize_subprocess_env\n+from hermes_cli import _subprocess_compat\n from hermes_cli._subprocess_compat import windows_hide_flags\n from dataclasses import dataclass, field\n from typing import Any, Dict, List, Optional","@@ -569,12 +570,11 @@ def _terminate_host_pid(cls, pid: int, expected_start: Optional[int] = None) ->\n             return\n         if _IS_WINDOWS:\n             try:\n-                subprocess.run(\n+                _subprocess_compat.run(\n                     [\"taskkill\", \"/PID\", str(pid), \"/T\", \"/F\"],\n                     capture_output=True,\n                     text=True,\n                     timeout=10,\n-                    creationflags=windows_hide_flags(),\n                     stdin=subprocess.DEVNULL,\n                 )\n             except (FileNotFoundError, subprocess.TimeoutExpired, OSError):"]},{"file":"tools/transcription_tools.py","hunks":["@@ -37,6 +37,7 @@\n from typing import Optional, Dict, Any\n from urllib.parse import urljoin\n \n+from hermes_cli import _subprocess_compat\n from utils import is_truthy_value\n from tools.managed_tool_gateway import resolve_managed_tool_gateway\n from tools.tool_backend_helpers import (","@@ -485,7 +486,7 @@ def _terminate_command_stt_process_tree(proc: subprocess.Popen) -> None:\n \n     if os.name == \"nt\":\n         try:\n-            subprocess.run(\n+            _subprocess_compat.run(\n                 [\"taskkill\", \"/F\", \"/T\", \"/PID\", str(proc.pid)],\n                 stdout=subprocess.DEVNULL,\n                 stderr=subprocess.DEVNULL,","@@ -556,7 +557,7 @@ def _run_command_stt(command: str, timeout: float) -> subprocess.CompletedProces\n     else:\n         popen_kwargs[\"start_new_session\"] = True\n \n-    proc = subprocess.Popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)\n+    proc = _subprocess_compat.popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)\n     try:\n         stdout, stderr = proc.communicate(timeout=timeout)\n     except subprocess.TimeoutExpired as exc:","@@ -1187,7 +1188,7 @@ def _prepare_local_audio(file_path: str, work_dir: str) -> tuple[Optional[str],\n     command = [ffmpeg, \"-y\", \"-i\", file_path, converted_path]\n \n     try:\n-        subprocess.run(command, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n+        _subprocess_compat.run(command, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n         return converted_path, None\n     except subprocess.TimeoutExpired:\n         logger.error(\"ffmpeg conversion timed out for %s\", file_path)","@@ -1233,9 +1234,9 @@ def _transcribe_local_command(file_path: str, model_name: str) -> Dict[str, Any]\n             # User-provided templates (env var) may contain shell syntax; auto-detected commands are safe for list mode.\n             use_shell = bool(os.getenv(LOCAL_STT_COMMAND_ENV, \"\").strip())\n             if use_shell:\n-                subprocess.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n+                _subprocess_compat.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n             else:\n-                subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n+                _subprocess_compat.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n             \n \n             txt_files = sorted(Path(output_dir).glob(\"*.txt\"))"]},{"file":"tools/tts_tool.py","hunks":["@@ -52,6 +52,7 @@\n from typing import Callable, Dict, Any, Optional\n from urllib.parse import urljoin\n \n+from hermes_cli import _subprocess_compat\n from hermes_constants import display_hermes_home\n \n logger = logging.getLogger(__name__)","@@ -714,7 +715,7 @@ def _terminate_command_tts_process_tree(proc: subprocess.Popen) -> None:\n \n     if os.name == \"nt\":\n         try:\n-            subprocess.run(\n+            _subprocess_compat.run(\n                 [\"taskkill\", \"/F\", \"/T\", \"/PID\", str(proc.pid)],\n                 stdout=subprocess.DEVNULL,\n                 stderr=subprocess.DEVNULL,","@@ -772,7 +773,7 @@ def _run_command_tts(command: str, timeout: float) -> subprocess.CompletedProces\n     else:\n         popen_kwargs[\"start_new_session\"] = True\n \n-    proc = subprocess.Popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)\n+    proc = _subprocess_compat.popen(command, **popen_kwargs, stdin=subprocess.DEVNULL)\n     try:\n         stdout, stderr = proc.communicate(timeout=timeout)\n     except subprocess.TimeoutExpired as exc:","@@ -905,7 +906,7 @@ def _convert_to_opus(mp3_path: str) -> Optional[str]:\n \n     ogg_path = mp3_path.rsplit(\".\", 1)[0] + \".ogg\"\n     try:\n-        result = subprocess.run(\n+        result = _subprocess_compat.run(\n             [\"ffmpeg\", \"-i\", mp3_path, \"-acodec\", \"libopus\",\n              \"-ac\", \"1\", \"-b:a\", \"64k\", \"-vbr\", \"off\", ogg_path, \"-y\"],\n             capture_output=True, timeout=30,","@@ -1776,7 +1777,7 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any]\n                 ]\n             else:\n                 cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            result = subprocess.run(cmd, capture_output=True, timeout=30, stdin=subprocess.DEVNULL)\n+            result = _subprocess_compat.run(cmd, capture_output=True, timeout=30, stdin=subprocess.DEVNULL)\n             if result.returncode != 0:\n                 stderr = result.stderr.decode(\"utf-8\", errors=\"ignore\")[:300]\n                 raise RuntimeError(f\"ffmpeg conversion failed: {stderr}\")","@@ -1859,7 +1860,7 @@ def _generate_neutts(text: str, output_path: str, tts_config: Dict[str, Any]) ->\n         \"--device\", device,\n     ]\n \n-    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)\n+    result = _subprocess_compat.run(cmd, capture_output=True, text=True, timeout=120, stdin=subprocess.DEVNULL)\n     if result.returncode != 0:\n         stderr = result.stderr.strip()\n         # Filter out the \"OK:\" line from stderr","@@ -1871,7 +1872,7 @@ def _generate_neutts(text: str, output_path: str, tts_config: Dict[str, Any]) ->\n         ffmpeg = shutil.which(\"ffmpeg\")\n         if ffmpeg:\n             conv_cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n+            _subprocess_compat.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)\n             os.remove(wav_path)\n         else:\n             # No ffmpeg — just rename the WAV to the expected path","@@ -1938,7 +1939,7 @@ def _resolve_piper_voice_path(voice: str, download_dir: Path) -> str:\n     import sys as _sys\n     logger.info(\"[Piper] Downloading voice '%s' to %s (first use)\", voice, download_dir)\n     try:\n-        result = subprocess.run(\n+        result = _subprocess_compat.run(\n             [_sys.executable, \"-m\", \"piper.download_voices\", voice,\n              \"--download-dir\", str(download_dir)],\n             capture_output=True, text=True, timeout=300,","@@ -2050,7 +2051,7 @@ def _generate_piper_tts(text: str, output_path: str, tts_config: Dict[str, Any])\n         ffmpeg = shutil.which(\"ffmpeg\")\n         if ffmpeg:\n             conv_cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n+            _subprocess_compat.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)\n             try:\n                 os.remove(wav_path)\n             except OSError:","@@ -2116,7 +2117,7 @@ def _generate_kittentts(text: str, output_path: str, tts_config: Dict[str, Any])\n         ffmpeg = shutil.which(\"ffmpeg\")\n         if ffmpeg:\n             conv_cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n+            _subprocess_compat.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)\n             os.remove(wav_path)\n         else:\n             # No ffmpeg — rename the WAV to the expected path","@@ -2812,7 +2813,6 @@ def _check(importer, label):\n # Registry\n # ---------------------------------------------------------------------------\n from tools.registry import registry, tool_error\n-from hermes_cli._subprocess_compat import windows_hide_flags\n \n TTS_SCHEMA = {\n     \"name\": \"text_to_speech\","]},{"file":"tui_gateway/entry.py","hunks":["@@ -260,6 +260,13 @@ def join_mcp_discovery(timeout: float | None = None) -> bool:\n \n \n def main():\n+    # Stdio backend spawned by Node/Electron: drop any console a uv\n+    # pythonw→python re-exec auto-allocated. No-op on POSIX.\n+    try:\n+        hermes_bootstrap.detach_orphan_console()\n+    except Exception:\n+        pass\n+\n     _install_sidecar_publisher()\n \n     # MCP tool discovery — runs in a background daemon thread so a slow or"]},{"file":"tui_gateway/git_probe.py","hunks":["@@ -46,7 +46,9 @@ def run_git(cwd: str, *args: str) -> str:\n     if not cwd:\n         return \"\"\n     try:\n-        result = subprocess.run(\n+        from hermes_cli import _subprocess_compat\n+\n+        result = _subprocess_compat.run(\n             [\"git\", \"-C\", cwd, *args],\n             capture_output=True,\n             text=True,"]},{"file":"tui_gateway/slash_worker.py","hunks":["@@ -93,6 +93,14 @@ def _run(cli: HermesCLI, command: str) -> str:\n \n \n def main():\n+    # Stdio worker spawned by the gateway: drop any console a uv pythonw→python\n+    # re-exec auto-allocated. No-op on POSIX.\n+    try:\n+        import hermes_bootstrap\n+        hermes_bootstrap.detach_orphan_console()\n+    except Exception:\n+        pass\n+\n     p = argparse.ArgumentParser(add_help=False)\n     p.add_argument(\"--session-key\", required=True)\n     p.add_argument(\"--model\", default=\"\")\n"]}]},{"kind":"pr","number":53879,"title":"fix(windows): hide backend subprocess windows by default","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54220","number":54220},{"kind":"pr","value":"#54236","number":54236},{"kind":"pr","value":"#53892","number":53892},{"kind":"pr","value":"#54417","number":54417}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":53892,"title":"fix(windows): hide backend console-flash legs main missed (Electron base-pythonw, ffmpeg, netstat)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["apps/desktop/electron/main.cjs","apps/desktop/electron/windows-child-process.test.cjs","plugins/platforms/discord/adapter.py","plugins/platforms/whatsapp/adapter.py","tests/test_windows_subprocess_no_window_flags.py","tools/transcription_tools.py","tools/tts_tool.py"],"fix_hunks":[{"file":"apps/desktop/electron/main.cjs","hunks":["@@ -1572,19 +1572,17 @@ function readVenvHome(venvRoot) {\n function getNoConsoleVenvPython(venvRoot) {\n   if (!IS_WINDOWS) return getVenvPython(venvRoot)\n \n-  // Prefer the venv's own pythonw shim — it carries pyvenv.cfg / site-packages\n-  // wiring. Falling back to the base uv/python.org pythonw.exe skips the venv\n-  // and breaks imports (yaml, hermes_cli, …) even when PYTHONPATH is patched.\n-  const venvPythonw = path.join(venvRoot, 'Scripts', 'pythonw.exe')\n-  if (fileExists(venvPythonw)) return venvPythonw\n-\n+  // The venv's ``Scripts\\pythonw.exe`` is a uv launcher shim that re-execs the\n+  // base console ``python.exe``, allocating a conhost/Windows Terminal window\n+  // that CREATE_NO_WINDOW can't suppress. Use the base ``pythonw.exe`` directly;\n+  // callers put the venv site-packages on PYTHONPATH so imports still resolve.\n   const baseHome = readVenvHome(venvRoot)\n   if (baseHome) {\n     const basePythonw = path.join(baseHome, 'pythonw.exe')\n     if (fileExists(basePythonw)) return basePythonw\n   }\n \n-  return venvPythonw\n+  return path.join(venvRoot, 'Scripts', 'pythonw.exe')\n }\n \n function toNoConsolePython(pythonPath) {","@@ -2847,7 +2845,7 @@ function createPythonBackend(root, label, dashboardArgs, options = {}) {\n     args: ['-m', 'hermes_cli.main', ...dashboardArgs],\n     env: buildDesktopBackendEnv({\n       hermesHome: HERMES_HOME,\n-      pythonPathEntries: [root],\n+      pythonPathEntries: [root, ...getVenvSitePackagesEntries(venvRoot)],\n       venvRoot\n     }),\n     root,","@@ -2871,7 +2869,7 @@ function createActiveBackend(dashboardArgs) {\n     args: ['-m', 'hermes_cli.main', ...dashboardArgs],\n     env: buildDesktopBackendEnv({\n       hermesHome: HERMES_HOME,\n-      pythonPathEntries: [ACTIVE_HERMES_ROOT],\n+      pythonPathEntries: [ACTIVE_HERMES_ROOT, ...getVenvSitePackagesEntries(VENV_ROOT)],\n       venvRoot: VENV_ROOT\n     }),\n     root: ACTIVE_HERMES_ROOT,"]},{"file":"apps/desktop/electron/windows-child-process.test.cjs","hunks":["@@ -53,6 +53,22 @@ test('desktop background child processes opt into hidden Windows consoles', () =\n   assert.match(source, /args: \\['-m', 'hermes_cli\\.main', \\.\\.\\.dashboardArgs\\]/)\n })\n \n+test('getNoConsoleVenvPython prefers base pythonw over the uv re-exec shim', () => {\n+  const source = readElectronFile('main.cjs')\n+  const body = source.slice(\n+    source.indexOf('function getNoConsoleVenvPython(venvRoot)'),\n+    source.indexOf('function getVenvSitePackagesEntries(venvRoot)')\n+  )\n+\n+  // The venv Scripts\\pythonw.exe re-execs a console python.exe (flashes a\n+  // conhost); the base pythonw must be resolved first so it never runs.\n+  const baseIdx = body.indexOf('basePythonw')\n+  const shimIdx = body.indexOf(\"'Scripts', 'pythonw.exe'\")\n+  assert.notEqual(baseIdx, -1, 'base pythonw resolution missing')\n+  assert.notEqual(shimIdx, -1, 'venv shim fallback missing')\n+  assert.ok(baseIdx < shimIdx, 'base pythonw must be preferred before the venv Scripts shim')\n+})\n+\n test('intentional or interactive desktop child processes stay documented', () => {\n   const source = readElectronFile('main.cjs')\n "]},{"file":"plugins/platforms/discord/adapter.py","hunks":["@@ -665,6 +665,8 @@ def pcm_to_wav(pcm_data: bytes, output_path: str,\n             f.write(pcm_data)\n             pcm_path = f.name\n         try:\n+            from hermes_cli._subprocess_compat import windows_hide_flags\n+\n             subprocess.run(\n                 [\n                     \"ffmpeg\", \"-y\", \"-loglevel\", \"error\",","@@ -679,6 +681,7 @@ def pcm_to_wav(pcm_data: bytes, output_path: str,\n                 check=True,\n                 timeout=10,\n                 stdin=subprocess.DEVNULL,\n+                creationflags=windows_hide_flags(),\n             )\n         finally:\n             try:"]},{"file":"plugins/platforms/whatsapp/adapter.py","hunks":["@@ -78,10 +78,13 @@ def _kill_port_process(port: int) -> None:\n     \"\"\"Kill any process *listening* on the given TCP port (a stale bridge).\"\"\"\n     try:\n         if _IS_WINDOWS:\n+            from hermes_cli._subprocess_compat import windows_hide_flags\n+\n             # Use netstat to find the PID bound to this port, then taskkill\n             result = subprocess.run(\n                 [\"netstat\", \"-ano\", \"-p\", \"TCP\"],\n                 capture_output=True, text=True, timeout=5,\n+                creationflags=windows_hide_flags(),\n             )\n             for line in result.stdout.splitlines():\n                 parts = line.split()","@@ -92,6 +95,7 @@ def _kill_port_process(port: int) -> None:\n                             subprocess.run(\n                                 [\"taskkill\", \"/PID\", parts[4], \"/F\"],\n                                 capture_output=True, timeout=5,\n+                                creationflags=windows_hide_flags(),\n                             )\n                         except subprocess.SubprocessError:\n                             pass"]},{"file":"tests/test_windows_subprocess_no_window_flags.py","hunks":["@@ -261,3 +261,41 @@ def fake_run(cmd, **kwargs):\n     assert skill_preprocessing.run_inline_shell(\"echo ok\", cwd=None, timeout=5) == \"ok\"\n     assert captured[0][0] == [\"bash\", \"-c\", \"echo ok\"]\n     assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+\n+def test_tts_opus_conversion_hides_ffmpeg_window(monkeypatch, tmp_path):\n+    from tools import tts_tool\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(returncode=0)\n+\n+    monkeypatch.setattr(tts_tool, \"_has_ffmpeg\", lambda: True)\n+    monkeypatch.setattr(tts_tool, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(tts_tool.subprocess, \"run\", fake_run)\n+\n+    tts_tool._convert_to_opus(str(tmp_path / \"v.mp3\"))\n+\n+    assert captured[0][0][0] == \"ffmpeg\"\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+\n+def test_local_stt_audio_prep_hides_ffmpeg_window(monkeypatch, tmp_path):\n+    from tools import transcription_tools\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(returncode=0)\n+\n+    monkeypatch.setattr(transcription_tools, \"_find_ffmpeg_binary\", lambda: \"ffmpeg\")\n+    monkeypatch.setattr(transcription_tools, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(transcription_tools.subprocess, \"run\", fake_run)\n+\n+    transcription_tools._prepare_local_audio(str(tmp_path / \"in.m4a\"), str(tmp_path))\n+\n+    assert captured[0][0][0] == \"ffmpeg\"\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW"]},{"file":"tools/transcription_tools.py","hunks":["@@ -37,6 +37,7 @@\n from typing import Optional, Dict, Any\n from urllib.parse import urljoin\n \n+from hermes_cli._subprocess_compat import windows_hide_flags\n from utils import is_truthy_value\n from tools.managed_tool_gateway import resolve_managed_tool_gateway\n from tools.tool_backend_helpers import (","@@ -1187,7 +1188,7 @@ def _prepare_local_audio(file_path: str, work_dir: str) -> tuple[Optional[str],\n     command = [ffmpeg, \"-y\", \"-i\", file_path, converted_path]\n \n     try:\n-        subprocess.run(command, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n+        subprocess.run(command, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n         return converted_path, None\n     except subprocess.TimeoutExpired:\n         logger.error(\"ffmpeg conversion timed out for %s\", file_path)","@@ -1233,9 +1234,9 @@ def _transcribe_local_command(file_path: str, model_name: str) -> Dict[str, Any]\n             # User-provided templates (env var) may contain shell syntax; auto-detected commands are safe for list mode.\n             use_shell = bool(os.getenv(LOCAL_STT_COMMAND_ENV, \"\").strip())\n             if use_shell:\n-                subprocess.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n+                subprocess.run(command, shell=True, check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n             else:\n-                subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL)\n+                subprocess.run(shlex.split(command), check=True, capture_output=True, text=True, timeout=300, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n             \n \n             txt_files = sorted(Path(output_dir).glob(\"*.txt\"))"]},{"file":"tools/tts_tool.py","hunks":["@@ -52,6 +52,7 @@\n from typing import Callable, Dict, Any, Optional\n from urllib.parse import urljoin\n \n+from hermes_cli._subprocess_compat import windows_hide_flags\n from hermes_constants import display_hermes_home\n \n logger = logging.getLogger(__name__)","@@ -910,6 +911,7 @@ def _convert_to_opus(mp3_path: str) -> Optional[str]:\n              \"-ac\", \"1\", \"-b:a\", \"64k\", \"-vbr\", \"off\", ogg_path, \"-y\"],\n             capture_output=True, timeout=30,\n             stdin=subprocess.DEVNULL,\n+            creationflags=windows_hide_flags(),\n         )\n         if result.returncode != 0:\n             logger.warning(\"ffmpeg conversion failed with return code %d: %s\", ","@@ -1776,7 +1778,7 @@ def _generate_gemini_tts(text: str, output_path: str, tts_config: Dict[str, Any]\n                 ]\n             else:\n                 cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            result = subprocess.run(cmd, capture_output=True, timeout=30, stdin=subprocess.DEVNULL)\n+            result = subprocess.run(cmd, capture_output=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n             if result.returncode != 0:\n                 stderr = result.stderr.decode(\"utf-8\", errors=\"ignore\")[:300]\n                 raise RuntimeError(f\"ffmpeg conversion failed: {stderr}\")","@@ -1871,7 +1873,7 @@ def _generate_neutts(text: str, output_path: str, tts_config: Dict[str, Any]) ->\n         ffmpeg = shutil.which(\"ffmpeg\")\n         if ffmpeg:\n             conv_cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)\n+            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n             os.remove(wav_path)\n         else:\n             # No ffmpeg — just rename the WAV to the expected path","@@ -2050,7 +2052,7 @@ def _generate_piper_tts(text: str, output_path: str, tts_config: Dict[str, Any])\n         ffmpeg = shutil.which(\"ffmpeg\")\n         if ffmpeg:\n             conv_cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)\n+            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n             try:\n                 os.remove(wav_path)\n             except OSError:","@@ -2116,7 +2118,7 @@ def _generate_kittentts(text: str, output_path: str, tts_config: Dict[str, Any])\n         ffmpeg = shutil.which(\"ffmpeg\")\n         if ffmpeg:\n             conv_cmd = [ffmpeg, \"-i\", wav_path, \"-y\", \"-loglevel\", \"error\", output_path]\n-            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL)\n+            subprocess.run(conv_cmd, check=True, timeout=30, stdin=subprocess.DEVNULL, creationflags=windows_hide_flags())\n             os.remove(wav_path)\n         else:\n             # No ffmpeg — rename the WAV to the expected path\n"]}]},{"kind":"pr","number":54236,"title":"fix(windows): hide console-window flash on backend git/gh/wmic/bash subprocess spawns","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/coding_context.py","agent/context_references.py","agent/shell_hooks.py","agent/skill_preprocessing.py","gateway/status.py","hermes_cli/copilot_auth.py","hermes_cli/gateway.py","hermes_cli/main.py","tests/gateway/test_status.py","tests/test_windows_subprocess_no_window_flags.py","tui_gateway/git_probe.py","tui_gateway/server.py"],"fix_hunks":[{"file":"agent/coding_context.py","hunks":["@@ -60,6 +60,8 @@\n from pathlib import Path\n from typing import Any, Optional\n \n+from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags\n+\n logger = logging.getLogger(\"hermes.coding_context\")\n \n CODING_TOOLSET = \"coding\"","@@ -647,12 +649,14 @@ def _enabled_mcp_servers(config: Optional[dict[str, Any]]) -> list[str]:\n \n \n def _git(cwd: Path, *args: str) -> str:\n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     try:\n         out = subprocess.run(\n             [\"git\", \"-C\", str(cwd), *args],\n             capture_output=True,\n             text=True,\n             timeout=_GIT_TIMEOUT,\n+            **_popen_kwargs,\n         )\n     except (OSError, subprocess.SubprocessError):\n         return \"\""]},{"file":"agent/context_references.py","hunks":["@@ -12,6 +12,7 @@\n from typing import Awaitable, Callable\n \n from agent.model_metadata import estimate_tokens_rough\n+from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags\n \n _QUOTED_REFERENCE_VALUE = r'(?:`[^`\\n]+`|\"[^\"\\n]+\"|\\'[^\\'\\n]+\\')'\n REFERENCE_PATTERN = re.compile(","@@ -290,6 +291,7 @@ def _expand_git_reference(\n     args: list[str],\n     label: str,\n ) -> tuple[str | None, str | None]:\n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     try:\n         result = subprocess.run(\n             [\"git\", *args],","@@ -298,6 +300,7 @@ def _expand_git_reference(\n             text=True,\n             timeout=30,\n             stdin=subprocess.DEVNULL,\n+            **_popen_kwargs,\n         )\n     except subprocess.TimeoutExpired:\n         return f\"{ref.raw}: git command timed out (30s)\", None","@@ -483,6 +486,7 @@ def _iter_visible_entries(path: Path, cwd: Path, limit: int) -> list[Path]:\n \n \n def _rg_files(path: Path, cwd: Path, limit: int) -> list[Path] | None:\n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     try:\n         result = subprocess.run(\n             [\"rg\", \"--files\", str(path.relative_to(cwd))],","@@ -491,6 +495,7 @@ def _rg_files(path: Path, cwd: Path, limit: int) -> list[Path] | None:\n             text=True,\n             timeout=10,\n             stdin=subprocess.DEVNULL,\n+            **_popen_kwargs,\n         )\n     except (FileNotFoundError, OSError, subprocess.TimeoutExpired):\n         return None"]},{"file":"agent/shell_hooks.py","hunks":["@@ -122,6 +122,8 @@\n from pathlib import Path\n from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple\n \n+from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags\n+\n try:\n     import fcntl  # POSIX only; Windows falls back to best-effort without flock.\n except ImportError:  # pragma: no cover","@@ -441,6 +443,7 @@ def _spawn(spec: ShellHookSpec, stdin_json: str) -> Dict[str, Any]:\n         return result\n \n     t0 = time.monotonic()\n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     try:\n         proc = subprocess.run(\n             argv,","@@ -449,6 +452,7 @@ def _spawn(spec: ShellHookSpec, stdin_json: str) -> Dict[str, Any]:\n             timeout=spec.timeout,\n             text=True,\n             shell=False,\n+            **_popen_kwargs,\n         )\n     except subprocess.TimeoutExpired:\n         result[\"timed_out\"] = True"]},{"file":"agent/skill_preprocessing.py","hunks":["@@ -5,6 +5,8 @@\n import subprocess\n from pathlib import Path\n \n+from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags\n+\n logger = logging.getLogger(__name__)\n \n # Matches ${HERMES_SKILL_DIR} / ${HERMES_SESSION_ID} tokens in SKILL.md.","@@ -66,6 +68,7 @@ def run_inline_shell(command: str, cwd: Path | None, timeout: int) -> str:\n     Failures return a short ``[inline-shell error: ...]`` marker instead of\n     raising, so one bad snippet can't wreck the whole skill message.\n     \"\"\"\n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     try:\n         completed = subprocess.run(\n             [\"bash\", \"-c\", command],","@@ -75,6 +78,7 @@ def run_inline_shell(command: str, cwd: Path | None, timeout: int) -> str:\n             timeout=max(1, int(timeout)),\n             check=False,\n             stdin=subprocess.DEVNULL,\n+            **_popen_kwargs,\n         )\n     except subprocess.TimeoutExpired:\n         return f\"[inline-shell timeout after {timeout}s: {command}]\""]},{"file":"gateway/status.py","hunks":["@@ -81,12 +81,18 @@ def terminate_pid(pid: int, *, force: bool = False) -> None:\n     because os.kill(..., SIGTERM) is not equivalent to a tree-killing hard stop.\n     \"\"\"\n     if force and _IS_WINDOWS:\n+        # CREATE_NO_WINDOW: terminate_pid runs from the windowless pythonw.exe\n+        # gateway/desktop backend, so a bare taskkill spawn would flash a\n+        # conhost window on every force-kill.\n+        from hermes_cli._subprocess_compat import windows_hide_flags\n+\n         try:\n             result = subprocess.run(\n                 [\"taskkill\", \"/PID\", str(pid), \"/T\", \"/F\"],\n                 capture_output=True,\n                 text=True,\n                 timeout=10,\n+                creationflags=windows_hide_flags(),\n             )\n         except FileNotFoundError:\n             os.kill(pid, signal.SIGTERM)"]},{"file":"hermes_cli/copilot_auth.py","hunks":["@@ -27,6 +27,8 @@\n from pathlib import Path\n from typing import Optional\n \n+from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags\n+\n logger = logging.getLogger(__name__)\n \n # OAuth device code flow constants (same client ID as opencode/Copilot CLI)","@@ -130,6 +132,7 @@ def _try_gh_cli_token() -> Optional[str]:\n     clean_env = {k: v for k, v in os.environ.items()\n                  if k not in {\"GITHUB_TOKEN\", \"GH_TOKEN\"}}\n \n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     for gh_path in _gh_cli_candidates():\n         cmd = [gh_path, \"auth\", \"token\"]\n         if hostname:","@@ -141,6 +144,7 @@ def _try_gh_cli_token() -> Optional[str]:\n                 text=True,\n                 timeout=5,\n                 env=clean_env,\n+                **_popen_kwargs,\n             )\n         except (FileNotFoundError, subprocess.TimeoutExpired) as exc:\n             logger.debug(\"gh CLI token lookup failed (%s): %s\", gh_path, exc)"]},{"file":"hermes_cli/gateway.py","hunks":["@@ -384,6 +384,12 @@ def _matches_gateway_runtime(command: str) -> bool:\n             # removed as part of the WMIC deprecation — fall back to\n             # PowerShell's Get-CimInstance.  Any OSError here (FileNotFoundError\n             # on missing wmic) trips the fallback.\n+            # Hide the console window: this scan runs inside the windowless\n+            # pythonw.exe gateway/desktop backend, so a bare wmic/powershell\n+            # spawn would flash a conhost window on every watchdog probe.\n+            from hermes_cli._subprocess_compat import windows_hide_flags\n+\n+            _no_window = {\"creationflags\": windows_hide_flags()}\n             wmic_path = shutil.which(\"wmic\")\n             used_fallback = False\n             result = None","@@ -402,6 +408,7 @@ def _matches_gateway_runtime(command: str) -> bool:\n                         encoding=\"utf-8\",\n                         errors=\"ignore\",\n                         timeout=10,\n+                        **_no_window,\n                     )\n                 except (OSError, subprocess.TimeoutExpired):\n                     result = None","@@ -427,6 +434,7 @@ def _matches_gateway_runtime(command: str) -> bool:\n                         encoding=\"utf-8\",\n                         errors=\"ignore\",\n                         timeout=15,\n+                        **_no_window,\n                     )\n                 except (OSError, subprocess.TimeoutExpired):\n                     return []"]},{"file":"hermes_cli/main.py","hunks":["@@ -5825,6 +5825,11 @@ def _find_stale_dashboard_pids(\n             # here is errors=\"ignore\": it prevents a reader-thread\n             # UnicodeDecodeError from leaving result.stdout=None and turning\n             # the later .split() into an AttributeError (#17049).\n+            # CREATE_NO_WINDOW hides the conhost flash: this scan can run from\n+            # the windowless pythonw.exe desktop/gateway backend during an\n+            # update, where a bare wmic spawn would pop a console window.\n+            from hermes_cli._subprocess_compat import windows_hide_flags\n+\n             result = subprocess.run(\n                 [\"wmic\", \"process\", \"get\", \"ProcessId,CommandLine\", \"/FORMAT:LIST\"],\n                 capture_output=True,","@@ -5832,6 +5837,7 @@ def _find_stale_dashboard_pids(\n                 timeout=10,\n                 encoding=\"utf-8\",\n                 errors=\"ignore\",\n+                creationflags=windows_hide_flags(),\n             )\n             if result.returncode != 0 or result.stdout is None:\n                 return []"]},{"file":"tests/gateway/test_status.py","hunks":["@@ -621,16 +621,22 @@ def test_force_uses_taskkill_on_windows(self, monkeypatch):\n         calls = []\n         monkeypatch.setattr(status, \"_IS_WINDOWS\", True)\n \n-        def fake_run(cmd, capture_output=False, text=False, timeout=None):\n-            calls.append((cmd, capture_output, text, timeout))\n+        def fake_run(cmd, capture_output=False, text=False, timeout=None, creationflags=0):\n+            calls.append((cmd, capture_output, text, timeout, creationflags))\n             return SimpleNamespace(returncode=0, stdout=\"\", stderr=\"\")\n \n         monkeypatch.setattr(status.subprocess, \"run\", fake_run)\n \n         status.terminate_pid(123, force=True)\n \n+        # taskkill is spawned with the no-window flag so the windowless\n+        # pythonw.exe backend doesn't flash a conhost window on force-kill.\n+        # windows_hide_flags() is 0 on the POSIX test host (a valid no-op\n+        # creationflags value); on real Windows it is CREATE_NO_WINDOW.\n+        from hermes_cli._subprocess_compat import windows_hide_flags\n+\n         assert calls == [\n-            ([\"taskkill\", \"/PID\", \"123\", \"/T\", \"/F\"], True, True, 10)\n+            ([\"taskkill\", \"/PID\", \"123\", \"/T\", \"/F\"], True, True, 10, windows_hide_flags())\n         ]\n \n     def test_force_falls_back_to_sigterm_when_taskkill_missing(self, monkeypatch):"]},{"file":"tests/test_windows_subprocess_no_window_flags.py","hunks":["@@ -0,0 +1,263 @@\n+from __future__ import annotations\n+\n+import subprocess\n+from pathlib import Path\n+from types import SimpleNamespace\n+\n+\n+_CREATE_NO_WINDOW = 0x08000000\n+\n+\n+class _Completed:\n+    def __init__(self, stdout: str | bytes = \"ok\\n\", returncode: int = 0):\n+        self.stdout = stdout\n+        self.stderr = \"\"\n+        self.returncode = returncode\n+\n+\n+def test_tui_gateway_git_probe_hides_git_windows(monkeypatch):\n+    from tui_gateway import git_probe\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(stdout=\"main\\n\")\n+\n+    monkeypatch.setattr(git_probe, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(git_probe, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(git_probe.subprocess, \"run\", fake_run)\n+\n+    assert git_probe.run_git(\"C:/repo\", \"branch\", \"--show-current\") == \"main\"\n+\n+    assert captured == [\n+        (\n+            [\"git\", \"-C\", \"C:/repo\", \"branch\", \"--show-current\"],\n+            {\n+                \"capture_output\": True,\n+                \"text\": True,\n+                \"encoding\": \"utf-8\",\n+                \"errors\": \"replace\",\n+                \"timeout\": git_probe._GIT_TIMEOUT,\n+                \"check\": False,\n+                \"stdin\": subprocess.DEVNULL,\n+                \"creationflags\": _CREATE_NO_WINDOW,\n+            },\n+        )\n+    ]\n+\n+\n+def test_tui_gateway_fuzzy_file_listing_hides_git_windows(monkeypatch):\n+    from hermes_cli import _subprocess_compat\n+    from tui_gateway import server\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        if cmd[-1] == \"--show-toplevel\":\n+            return _Completed(stdout=b\"C:/repo\\n\")\n+        return _Completed(stdout=b\"src/main.py\\0README.md\\0\")\n+\n+    monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(_subprocess_compat, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(server.subprocess, \"run\", fake_run)\n+    server._fuzzy_cache.clear()\n+\n+    assert server._list_repo_files(\"C:/repo\") == [\"src/main.py\", \"README.md\"]\n+\n+    assert [kwargs[\"creationflags\"] for _, kwargs in captured] == [\n+        _CREATE_NO_WINDOW,\n+        _CREATE_NO_WINDOW,\n+    ]\n+\n+\n+def test_coding_context_git_hides_git_windows(monkeypatch):\n+    from agent import coding_context\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(stdout=\"clean\\n\")\n+\n+    monkeypatch.setattr(coding_context, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(coding_context, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(coding_context.subprocess, \"run\", fake_run)\n+\n+    assert coding_context._git(Path(\"C:/repo\"), \"status\", \"--short\") == \"clean\"\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+\n+def test_context_reference_git_and_rg_hide_windows(monkeypatch):\n+    from agent import context_references\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        if cmd[0] == \"rg\":\n+            return _Completed(stdout=\"src/main.py\\n\")\n+        return _Completed(stdout=\"diff --git a/src/main.py b/src/main.py\\n\")\n+\n+    monkeypatch.setattr(context_references, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(context_references, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(context_references.subprocess, \"run\", fake_run)\n+\n+    ref = context_references.ContextReference(\n+        raw=\"@diff\",\n+        kind=\"diff\",\n+        target=\"\",\n+        start=0,\n+        end=5,\n+    )\n+    warning, block = context_references._expand_git_reference(\n+        ref,\n+        Path(\"C:/repo\"),\n+        [\"diff\"],\n+        \"git diff\",\n+    )\n+    assert warning is None\n+    assert block is not None\n+    assert \"git diff\" in block\n+    assert context_references._rg_files(Path(\"C:/repo/src\"), Path(\"C:/repo\"), 10) == [\n+        Path(\"src/main.py\")\n+    ]\n+\n+    assert [kwargs[\"creationflags\"] for _, kwargs in captured] == [\n+        _CREATE_NO_WINDOW,\n+        _CREATE_NO_WINDOW,\n+    ]\n+\n+\n+def test_copilot_gh_cli_probe_hides_gh_windows(monkeypatch):\n+    from hermes_cli import copilot_auth\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(stdout=\"gho_from_cli\\n\")\n+\n+    monkeypatch.setattr(copilot_auth, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(copilot_auth, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(copilot_auth, \"_gh_cli_candidates\", lambda: [\"gh\"])\n+    monkeypatch.setattr(copilot_auth.subprocess, \"run\", fake_run)\n+\n+    assert copilot_auth._try_gh_cli_token() == \"gho_from_cli\"\n+    assert captured[0][0] == [\"gh\", \"auth\", \"token\"]\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+\n+def test_gateway_pid_scan_hides_wmic_and_powershell_windows(monkeypatch):\n+    from hermes_cli import gateway\n+    from hermes_cli import _subprocess_compat\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        if cmd[0] == \"wmic\":\n+            return _Completed(stdout=\"\", returncode=1)\n+        return _Completed(stdout=\"CommandLine=hermes gateway\\nProcessId=123\\n\")\n+\n+    monkeypatch.setattr(gateway, \"is_windows\", lambda: True)\n+    monkeypatch.setattr(gateway.shutil, \"which\", lambda name: name)\n+    monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(_subprocess_compat, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(gateway.subprocess, \"run\", fake_run)\n+\n+    assert gateway._scan_gateway_pids(set()) == [123]\n+    assert [kwargs[\"creationflags\"] for _, kwargs in captured] == [\n+        _CREATE_NO_WINDOW,\n+        _CREATE_NO_WINDOW,\n+    ]\n+\n+\n+def test_stale_dashboard_windows_scan_hides_wmic(monkeypatch):\n+    from hermes_cli import main\n+    from hermes_cli import _subprocess_compat\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(stdout=\"CommandLine=hermes dashboard\\nProcessId=123\\n\")\n+\n+    monkeypatch.setattr(main.sys, \"platform\", \"win32\")\n+    monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(_subprocess_compat, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(main.subprocess, \"run\", fake_run)\n+\n+    assert main._find_stale_dashboard_pids() == [123]\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+\n+def test_gateway_force_kill_hides_taskkill_window(monkeypatch):\n+    from gateway import status\n+    from hermes_cli import _subprocess_compat\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Completed(stdout=\"\")\n+\n+    monkeypatch.setattr(status, \"_IS_WINDOWS\", True)\n+    monkeypatch.setattr(_subprocess_compat, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(_subprocess_compat, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(status.subprocess, \"run\", fake_run)\n+\n+    status.terminate_pid(123, force=True)\n+\n+    assert captured == [\n+        (\n+            [\"taskkill\", \"/PID\", \"123\", \"/T\", \"/F\"],\n+            {\n+                \"capture_output\": True,\n+                \"text\": True,\n+                \"timeout\": 10,\n+                \"creationflags\": _CREATE_NO_WINDOW,\n+            },\n+        )\n+    ]\n+\n+\n+def test_shell_hooks_hide_hook_command_windows(monkeypatch):\n+    from agent import shell_hooks\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return SimpleNamespace(returncode=0, stdout=\"{}\", stderr=\"\")\n+\n+    monkeypatch.setattr(shell_hooks, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(shell_hooks, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(shell_hooks.subprocess, \"run\", fake_run)\n+\n+    result = shell_hooks._spawn(\n+        shell_hooks.ShellHookSpec(event=\"post_tool_call\", command=\"hook-bin --flag\"),\n+        \"{}\",\n+    )\n+\n+    assert result[\"returncode\"] == 0\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+\n+def test_inline_skill_shell_hides_bash_window(monkeypatch):\n+    from agent import skill_preprocessing\n+\n+    captured = []\n+\n+    def fake_run(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return SimpleNamespace(returncode=0, stdout=\"ok\\n\", stderr=\"\")\n+\n+    monkeypatch.setattr(skill_preprocessing, \"IS_WINDOWS\", True)\n+    monkeypatch.setattr(skill_preprocessing, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+    monkeypatch.setattr(skill_preprocessing.subprocess, \"run\", fake_run)\n+\n+    assert skill_preprocessing.run_inline_shell(\"echo ok\", cwd=None, timeout=5) == \"ok\"\n+    assert captured[0][0] == [\"bash\", \"-c\", \"echo ok\"]\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW"]},{"file":"tui_gateway/git_probe.py","hunks":["@@ -31,6 +31,8 @@\n from collections.abc import Iterable\n from concurrent.futures import ThreadPoolExecutor\n \n+from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags\n+\n _GIT_TIMEOUT = 1.5\n _WARM_WORKERS = 8\n ","@@ -45,14 +47,18 @@ def run_git(cwd: str, *args: str) -> str:\n     \"\"\"``git -C <cwd> <args>`` → stripped stdout, or ``\"\"`` on any failure.\"\"\"\n     if not cwd:\n         return \"\"\n+    _popen_kwargs = {\"creationflags\": windows_hide_flags()} if IS_WINDOWS else {}\n     try:\n         result = subprocess.run(\n             [\"git\", \"-C\", cwd, *args],\n             capture_output=True,\n             text=True,\n+            encoding=\"utf-8\",\n+            errors=\"replace\",\n             timeout=_GIT_TIMEOUT,\n             check=False,\n             stdin=subprocess.DEVNULL,\n+            **_popen_kwargs,\n         )\n         return result.stdout.strip() if result.returncode == 0 else \"\"\n     except Exception:"]},{"file":"tui_gateway/server.py","hunks":["@@ -11668,6 +11668,9 @@ def _list_repo_files(root: str) -> list[str]:\n             return cached[1]\n \n     files: list[str] = []\n+    from hermes_cli._subprocess_compat import windows_hide_flags\n+\n+    _creationflags = windows_hide_flags()\n     try:\n         top_result = subprocess.run(\n             [\"git\", \"-C\", root, \"rev-parse\", \"--show-toplevel\"],","@@ -11675,6 +11678,7 @@ def _list_repo_files(root: str) -> list[str]:\n             timeout=2.0,\n             check=False,\n             stdin=subprocess.DEVNULL,\n+            creationflags=_creationflags,\n         )\n         if top_result.returncode == 0:\n             top = top_result.stdout.decode(\"utf-8\", \"replace\").strip()","@@ -11693,6 +11697,7 @@ def _list_repo_files(root: str) -> list[str]:\n                 timeout=2.0,\n                 check=False,\n                 stdin=subprocess.DEVNULL,\n+                creationflags=_creationflags,\n             )\n             if list_result.returncode == 0:\n                 for p in list_result.stdout.decode(\"utf-8\", \"replace\").split(\"\\0\"):\n"]}]},{"kind":"pr","number":54253,"title":"fix(win32): add CREATE_NO_WINDOW to all background subprocess calls","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"cb982ad99","number":0}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":54417,"title":"fix(windows): cover remaining console-flash spawn legs","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","gateway/status.py","hermes_cli/gateway.py","hermes_constants.py","tests/gateway/test_restart_drain.py","tests/gateway/test_status.py","tests/test_hermes_constants.py","tests/test_windows_subprocess_no_window_flags.py","tests/tools/test_browser_chromium_check.py","tests/tools/test_browser_homebrew_paths.py","tools/browser_tool.py","tui_gateway/server.py"],"fix_hunks":[{"file":"gateway/run.py","hunks":["@@ -5501,6 +5501,19 @@ def _alive(p):\n             # run as a self-restart loop guard and the gateway stays stopped.\n             watcher_env.pop(\"_HERMES_GATEWAY\", None)\n             project_root = Path(__file__).resolve().parent.parent\n+            watcher_python = sys.executable\n+            try:\n+                # Prefer a real GUI-subsystem interpreter for the watcher\n+                # itself.  With uv venvs, ``python.exe`` can re-exec the base\n+                # console interpreter and flash even when the Popen carries\n+                # CREATE_NO_WINDOW; pythonw.exe avoids console allocation.\n+                from hermes_cli.gateway_windows import _resolve_detached_python\n+\n+                watcher_python, _watcher_venv_dir, _watcher_site_packages = (\n+                    _resolve_detached_python(sys.executable)\n+                )\n+            except Exception:\n+                watcher_python = sys.executable\n             venv_dir = Path(watcher_env.get(\"VIRTUAL_ENV\") or project_root / \"venv\")\n             site_packages = venv_dir / \"Lib\" / \"site-packages\"\n             if site_packages.exists():","@@ -5510,7 +5523,7 @@ def _alive(p):\n                     pythonpath.append(watcher_env[\"PYTHONPATH\"])\n                 watcher_env[\"PYTHONPATH\"] = os.pathsep.join(dict.fromkeys(pythonpath))\n             subprocess.Popen(\n-                [sys.executable, \"-c\", watcher, str(current_pid), str(restart_after_s), *cmd_argv],\n+                [watcher_python, \"-c\", watcher, str(current_pid), str(restart_after_s), *cmd_argv],\n                 stdout=subprocess.DEVNULL,\n                 stderr=subprocess.DEVNULL,\n                 env=watcher_env,"]},{"file":"gateway/status.py","hunks":["@@ -171,17 +171,18 @@ def _read_process_cmdline(pid: int) -> Optional[str]:\n         if raw:\n             return raw.replace(b\"\\x00\", b\" \").decode(\"utf-8\", errors=\"ignore\").strip()\n \n-    try:\n-        result = subprocess.run(\n-            [\"ps\", \"-p\", str(pid), \"-o\", \"command=\"],\n-            capture_output=True,\n-            text=True,\n-            timeout=5,\n-        )\n-        if result.returncode == 0 and result.stdout.strip():\n-            return result.stdout.strip()\n-    except (OSError, subprocess.TimeoutExpired):\n-        pass\n+    if not _IS_WINDOWS:\n+        try:\n+            result = subprocess.run(\n+                [\"ps\", \"-p\", str(pid), \"-o\", \"command=\"],\n+                capture_output=True,\n+                text=True,\n+                timeout=5,\n+            )\n+            if result.returncode == 0 and result.stdout.strip():\n+                return result.stdout.strip()\n+        except (OSError, subprocess.TimeoutExpired):\n+            pass\n \n     # Windows fallback: psutil (already used by _pid_exists)\n     try:"]},{"file":"hermes_cli/gateway.py","hunks":["@@ -180,7 +180,11 @@ def _get_parent_pid(pid: int) -> int | None:\n         pass\n     except Exception:\n         return None\n-    # Fallback: shell out to ps (POSIX only — bare ``ps`` doesn't exist on Windows).\n+    # Fallback: shell out to ps (POSIX only).  Git Bash installs ``ps.exe`` on\n+    # Windows; running it from the windowless desktop/gateway backend flashes a\n+    # console, and psutil above is the authoritative Windows path anyway.\n+    if is_windows():\n+        return None\n     if not shutil.which(\"ps\"):\n         return None\n     try:"]},{"file":"hermes_constants.py","hunks":["@@ -317,11 +317,14 @@ def node_tool_runnable(path: str | None) -> bool:\n     import subprocess\n \n     try:\n+        from hermes_cli._subprocess_compat import windows_hide_flags\n+\n         result = subprocess.run(\n             [path, \"--version\"],\n             capture_output=True,\n             timeout=10,\n             env=with_hermes_node_path(),\n+            creationflags=windows_hide_flags(),\n         )\n     except (OSError, subprocess.TimeoutExpired, ValueError):\n         return False","@@ -562,11 +565,14 @@ def agent_browser_runnable(path: str | None) -> bool:\n     import subprocess\n \n     try:\n+        from hermes_cli._subprocess_compat import windows_hide_flags\n+\n         result = subprocess.run(\n             [path, \"--version\"],\n             capture_output=True,\n             timeout=10,\n             env=with_hermes_node_path(),\n+            creationflags=windows_hide_flags(),\n         )\n     except (OSError, subprocess.TimeoutExpired, ValueError):\n         return False"]},{"file":"tests/gateway/test_restart_drain.py","hunks":["@@ -353,6 +353,49 @@ def fake_popen(cmd, **kwargs):\n     assert kwargs[\"stderr\"] is subprocess.DEVNULL\n \n \n[redacted:email]\n+async def test_windows_detached_restart_uses_pythonw_for_watcher(monkeypatch, tmp_path):\n+    runner, _adapter = make_restart_runner()\n+    popen_calls = []\n+    venv_dir = tmp_path / \"venv\"\n+    site_packages = venv_dir / \"Lib\" / \"site-packages\"\n+    site_packages.mkdir(parents=True)\n+\n+    monkeypatch.setattr(gateway_run.sys, \"platform\", \"win32\")\n+    monkeypatch.setattr(gateway_run.sys, \"executable\", r\"C:\\venv\\Scripts\\python.exe\")\n+    monkeypatch.setattr(gateway_run, \"_resolve_hermes_bin\", lambda: [\"hermes\"])\n+    monkeypatch.setattr(gateway_run.os, \"getpid\", lambda: 321)\n+    monkeypatch.setenv(\"VIRTUAL_ENV\", str(venv_dir))\n+\n+    import hermes_cli._subprocess_compat as subprocess_compat\n+    import hermes_cli.gateway_windows as gateway_windows\n+\n+    monkeypatch.setattr(\n+        gateway_windows,\n+        \"_resolve_detached_python\",\n+        lambda _python: (r\"C:\\Python311\\pythonw.exe\", venv_dir, [str(site_packages)]),\n+    )\n+    monkeypatch.setattr(\n+        subprocess_compat,\n+        \"windows_detach_popen_kwargs\",\n+        lambda: {\"creationflags\": 0x08000008},\n+    )\n+\n+    def fake_popen(cmd, **kwargs):\n+        popen_calls.append((cmd, kwargs))\n+        return MagicMock()\n+\n+    monkeypatch.setattr(subprocess, \"Popen\", fake_popen)\n+\n+    await runner._launch_detached_restart_command()\n+\n+    assert len(popen_calls) == 1\n+    cmd, kwargs = popen_calls[0]\n+    assert cmd[0] == r\"C:\\Python311\\pythonw.exe\"\n+    assert cmd[-3:] == [\"hermes\", \"gateway\", \"restart\"]\n+    assert kwargs[\"creationflags\"] == 0x08000008\n+\n+\n # ── Shutdown notification tests ──────────────────────────────────────\n \n "]},{"file":"tests/gateway/test_status.py","hunks":["@@ -2,6 +2,7 @@\n \n import json\n import os\n+import sys\n from pathlib import Path\n from types import SimpleNamespace\n ","@@ -1351,6 +1352,7 @@ class TestReadProcessCmdlinePsFallback:\n \n     def test_ps_fallback_when_proc_unavailable(self, monkeypatch):\n         monkeypatch.setattr(status.Path, \"read_bytes\", lambda self: (_ for _ in ()).throw(FileNotFoundError))\n+        monkeypatch.setattr(status, \"_IS_WINDOWS\", False)\n         monkeypatch.setattr(\n             status.subprocess, \"run\",\n             lambda args, **kwargs: SimpleNamespace(returncode=0, stdout=\"/usr/libexec/bluetoothuserd\\n\"),","@@ -1360,6 +1362,7 @@ def test_ps_fallback_when_proc_unavailable(self, monkeypatch):\n \n     def test_ps_fallback_returns_none_on_failure(self, monkeypatch):\n         monkeypatch.setattr(status.Path, \"read_bytes\", lambda self: (_ for _ in ()).throw(FileNotFoundError))\n+        monkeypatch.setattr(status, \"_IS_WINDOWS\", False)\n         monkeypatch.setattr(\n             status.subprocess, \"run\",\n             lambda args, **kwargs: SimpleNamespace(returncode=1, stdout=\"\"),","@@ -1381,6 +1384,7 @@ def fake_read_bytes(self):\n \n     def test_ps_fallback_used_when_proc_returns_empty(self, monkeypatch):\n         monkeypatch.setattr(status.Path, \"read_bytes\", lambda self: b\"\")\n+        monkeypatch.setattr(status, \"_IS_WINDOWS\", False)\n         monkeypatch.setattr(\n             status.subprocess, \"run\",\n             lambda args, **kwargs: SimpleNamespace(returncode=0, stdout=\"python hermes_cli/main.py gateway run\\n\"),","@@ -1388,6 +1392,34 @@ def test_ps_fallback_used_when_proc_returns_empty(self, monkeypatch):\n         result = status._read_process_cmdline(12345)\n         assert \"hermes_cli/main.py\" in result\n \n+    def test_windows_skips_ps_fallback_and_uses_psutil(self, monkeypatch):\n+        monkeypatch.setattr(status.Path, \"read_bytes\", lambda self: (_ for _ in ()).throw(FileNotFoundError))\n+        monkeypatch.setattr(status, \"_IS_WINDOWS\", True)\n+        ps_calls = []\n+        monkeypatch.setattr(\n+            status.subprocess,\n+            \"run\",\n+            lambda args, **kwargs: ps_calls.append((args, kwargs)) or SimpleNamespace(returncode=0, stdout=\"ps should not run\\n\"),\n+        )\n+\n+        class _Proc:\n+            def __init__(self, pid):\n+                self.pid = pid\n+\n+            def cmdline(self):\n+                return [\"pythonw.exe\", \"-m\", \"hermes_cli.main\", \"gateway\", \"run\"]\n+\n+        monkeypatch.setitem(\n+            sys.modules,\n+            \"psutil\",\n+            SimpleNamespace(Process=_Proc),\n+        )\n+\n+        result = status._read_process_cmdline(12345)\n+\n+        assert result == \"pythonw.exe -m hermes_cli.main gateway run\"\n+        assert ps_calls == []\n+\n \n class TestCorruptStatusFiles:\n     \"\"\"A status / pid file holding non-UTF-8 (binary) bytes must read as"]},{"file":"tests/test_hermes_constants.py","hunks":["@@ -2,6 +2,7 @@\n \n import os\n from pathlib import Path\n+from types import SimpleNamespace\n \n import pytest\n ","@@ -611,6 +612,43 @@ def test_npx_fallback_form_accepted(self):\n         assert agent_browser_runnable(\"npx agent-browser\") is True\n         assert agent_browser_runnable(\"/usr/local/bin/npx agent-browser\") is True\n \n+    def test_version_probe_uses_windows_hide_flags(self, tmp_path, monkeypatch):\n+        good = self._stub(tmp_path, \"agent-browser\", \"#!/bin/sh\\necho hi\\n\")\n+        captured = []\n+\n+        def fake_run(cmd, **kwargs):\n+            captured.append((cmd, kwargs))\n+            return SimpleNamespace(returncode=0)\n+\n+        import hermes_cli._subprocess_compat as subprocess_compat\n+        import subprocess as subprocess_mod\n+\n+        monkeypatch.setattr(subprocess_compat, \"windows_hide_flags\", lambda: 0x08000000)\n+        monkeypatch.setattr(subprocess_mod, \"run\", fake_run)\n+\n+        assert agent_browser_runnable(str(good)) is True\n+        assert captured[0][0] == [str(good), \"--version\"]\n+        assert captured[0][1][\"creationflags\"] == 0x08000000\n+\n+\n+    def test_node_tool_probe_uses_windows_hide_flags(self, tmp_path, monkeypatch):\n+        good = self._stub(tmp_path, \"node\", \"#!/bin/sh\\necho v22\\n\")\n+        captured = []\n+\n+        def fake_run(cmd, **kwargs):\n+            captured.append((cmd, kwargs))\n+            return SimpleNamespace(returncode=0)\n+\n+        import hermes_cli._subprocess_compat as subprocess_compat\n+        import subprocess as subprocess_mod\n+\n+        monkeypatch.setattr(subprocess_compat, \"windows_hide_flags\", lambda: 0x08000000)\n+        monkeypatch.setattr(subprocess_mod, \"run\", fake_run)\n+\n+        assert node_tool_runnable(str(good)) is True\n+        assert captured[0][0] == [str(good), \"--version\"]\n+        assert captured[0][1][\"creationflags\"] == 0x08000000\n+\n \n class TestGetHermesDir:\n     \"\"\"Tests for ``get_hermes_dir(new_subpath, old_name)``."]},{"file":"tests/test_windows_subprocess_no_window_flags.py","hunks":["@@ -299,3 +299,29 @@ def fake_run(cmd, **kwargs):\n \n     assert captured[0][0][0] == \"ffmpeg\"\n     assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW\n+\n+def test_tui_slash_worker_hides_python_window(monkeypatch):\n+    from tui_gateway import server\n+\n+    captured = []\n+\n+    class _Proc:\n+        stdin = SimpleNamespace()\n+        stdout = []\n+        stderr = []\n+\n+    def fake_popen(cmd, **kwargs):\n+        captured.append((cmd, kwargs))\n+        return _Proc()\n+\n+    monkeypatch.setattr(server.subprocess, \"Popen\", fake_popen)\n+    monkeypatch.setattr(server.threading, \"Thread\", lambda *a, **k: SimpleNamespace(start=lambda: None))\n+\n+    import hermes_cli._subprocess_compat as subprocess_compat\n+\n+    monkeypatch.setattr(subprocess_compat, \"windows_hide_flags\", lambda: _CREATE_NO_WINDOW)\n+\n+    server._SlashWorker(\"session-key\", \"model-x\")\n+\n+    assert captured[0][0][:3] == [server.sys.executable, \"-m\", \"tui_gateway.slash_worker\"]\n+    assert captured[0][1][\"creationflags\"] == _CREATE_NO_WINDOW"]},{"file":"tests/tools/test_browser_chromium_check.py","hunks":["@@ -76,7 +76,7 @@ class TestCheckBrowserRequirementsChromium:\n \n     def test_local_mode_with_chromium_returns_true(self, monkeypatch, tmp_path):\n         monkeypatch.setattr(bt, \"_is_camofox_mode\", lambda: False)\n-        monkeypatch.setattr(bt, \"_find_agent_browser\", lambda: \"/usr/local/bin/agent-browser\")\n+        monkeypatch.setattr(bt, \"_find_agent_browser\", lambda **_kw: \"/usr/local/bin/agent-browser\")\n         monkeypatch.setattr(bt, \"_requires_real_termux_browser_install\", lambda _: False)\n         monkeypatch.setattr(bt, \"_get_cloud_provider\", lambda: None)\n         monkeypatch.setenv(\"PLAYWRIGHT_BROWSERS_PATH\", str(tmp_path))","@@ -93,7 +93,7 @@ def provider_name(self):\n                 return \"browserbase\"\n \n         monkeypatch.setattr(bt, \"_is_camofox_mode\", lambda: False)\n-        monkeypatch.setattr(bt, \"_find_agent_browser\", lambda: \"/usr/local/bin/agent-browser\")\n+        monkeypatch.setattr(bt, \"_find_agent_browser\", lambda **_kw: \"/usr/local/bin/agent-browser\")\n         monkeypatch.setattr(bt, \"_requires_real_termux_browser_install\", lambda _: False)\n         monkeypatch.setattr(bt, \"_get_cloud_provider\", lambda: FakeProvider())\n         # Point chromium search at an empty dir — should not matter for cloud.","@@ -102,6 +102,23 @@ def provider_name(self):\n \n         assert bt.check_browser_requirements() is True\n \n+    def test_startup_check_uses_lightweight_agent_browser_lookup(self, monkeypatch, tmp_path):\n+        seen = []\n+\n+        def fake_find_agent_browser(**kwargs):\n+            seen.append(kwargs)\n+            return \"/usr/local/bin/agent-browser\"\n+\n+        monkeypatch.setattr(bt, \"_is_camofox_mode\", lambda: False)\n+        monkeypatch.setattr(bt, \"_find_agent_browser\", fake_find_agent_browser)\n+        monkeypatch.setattr(bt, \"_requires_real_termux_browser_install\", lambda _: False)\n+        monkeypatch.setattr(bt, \"_get_cloud_provider\", lambda: None)\n+        monkeypatch.setenv(\"PLAYWRIGHT_BROWSERS_PATH\", str(tmp_path))\n+        (tmp_path / \"chromium-1208\").mkdir()\n+\n+        assert bt.check_browser_requirements() is True\n+        assert seen == [{\"validate\": False}]\n+\n     def test_camofox_mode_does_not_require_chromium(self, monkeypatch, tmp_path):\n         monkeypatch.setattr(bt, \"_is_camofox_mode\", lambda: True)\n         # Even with no chromium on disk, camofox drives its own backend."]},{"file":"tests/tools/test_browser_homebrew_paths.py","hunks":["@@ -222,7 +222,7 @@ def test_termux_requires_real_agent_browser_install_not_npx_fallback(self, monke\n         monkeypatch.setenv(\"PREFIX\", \"/data/data/com.termux/files/usr\")\n         monkeypatch.setattr(\"tools.browser_tool._is_camofox_mode\", lambda: False)\n         monkeypatch.setattr(\"tools.browser_tool._get_cloud_provider\", lambda: None)\n-        monkeypatch.setattr(\"tools.browser_tool._find_agent_browser\", lambda: \"npx agent-browser\")\n+        monkeypatch.setattr(\"tools.browser_tool._find_agent_browser\", lambda **_kw: \"npx agent-browser\")\n \n         assert check_browser_requirements() is False\n ","@@ -231,7 +231,7 @@ class TestRunBrowserCommandTermuxFallback:\n     def test_termux_local_mode_rejects_bare_npx_fallback(self, monkeypatch):\n         monkeypatch.setenv(\"TERMUX_VERSION\", \"0.118.3\")\n         monkeypatch.setenv(\"PREFIX\", \"/data/data/com.termux/files/usr\")\n-        monkeypatch.setattr(\"tools.browser_tool._find_agent_browser\", lambda: \"npx agent-browser\")\n+        monkeypatch.setattr(\"tools.browser_tool._find_agent_browser\", lambda **_kw: \"npx agent-browser\")\n         monkeypatch.setattr(\"tools.browser_tool._get_cloud_provider\", lambda: None)\n \n         result = _run_browser_command(\"task-1\", \"navigate\", [\"https://example.com\"])"]},{"file":"tools/browser_tool.py","hunks":["@@ -2002,7 +2002,15 @@ def _get_session_info(task_id: Optional[str] = None) -> Dict[str, str]:\n \n \n \n-def _find_agent_browser() -> str:\n+def _agent_browser_candidate_present(path: str | None) -> bool:\n+    if not path:\n+        return False\n+    if \" \" in path and path.split()[0].endswith(\"npx\"):\n+        return True\n+    return os.path.exists(path) and (os.name == \"nt\" or os.access(path, os.X_OK))\n+\n+\n+def _find_agent_browser(*, validate: bool = True) -> str:\n     \"\"\"\n     Find the agent-browser CLI executable.\n ","@@ -2041,7 +2049,11 @@ def _find_agent_browser() -> str:\n \n     # Check if it's in PATH (global install)\n     which_result = shutil.which(\"agent-browser\")\n-    if which_result and agent_browser_runnable(which_result):\n+    if which_result and (\n+        agent_browser_runnable(which_result) if validate else _agent_browser_candidate_present(which_result)\n+    ):\n+        if not validate:\n+            return which_result\n         _cached_agent_browser = which_result\n         _agent_browser_resolved = True\n         return which_result","@@ -2051,7 +2063,11 @@ def _find_agent_browser() -> str:\n     extended_path = _merge_browser_path(\"\")\n     if extended_path:\n         which_result = shutil.which(\"agent-browser\", path=extended_path)\n-        if which_result and agent_browser_runnable(which_result):\n+        if which_result and (\n+            agent_browser_runnable(which_result) if validate else _agent_browser_candidate_present(which_result)\n+        ):\n+            if not validate:\n+                return which_result\n             _cached_agent_browser = which_result\n             _agent_browser_resolved = True\n             return which_result","@@ -2068,7 +2084,11 @@ def _find_agent_browser() -> str:\n     local_bin_dir = repo_root / \"node_modules\" / \".bin\"\n     if local_bin_dir.is_dir():\n         local_which = shutil.which(\"agent-browser\", path=str(local_bin_dir))\n-        if local_which and agent_browser_runnable(local_which):\n+        if local_which and (\n+            agent_browser_runnable(local_which) if validate else _agent_browser_candidate_present(local_which)\n+        ):\n+            if not validate:\n+                return local_which\n             _cached_agent_browser = local_which\n             _agent_browser_resolved = True\n             return _cached_agent_browser","@@ -2078,10 +2098,15 @@ def _find_agent_browser() -> str:\n     if not npx_path and extended_path:\n         npx_path = shutil.which(\"npx\", path=extended_path)\n     if npx_path:\n+        if not validate:\n+            return \"npx agent-browser\"\n         _cached_agent_browser = \"npx agent-browser\"\n         _agent_browser_resolved = True\n         return _cached_agent_browser\n \n+    if not validate:\n+        raise FileNotFoundError(\"agent-browser CLI not found\")\n+\n     # Nothing found — try lazy installation before giving up.\n     try:\n         from hermes_cli.dep_ensure import ensure_dependency","@@ -4206,8 +4231,12 @@ def check_browser_requirements() -> bool:\n         return True\n \n     # The agent-browser CLI is required for local launch and cloud-provider flows.\n+    # Tool-schema assembly runs during Desktop startup; do not execute\n+    # ``agent-browser --version`` here, because Windows .cmd shims route through\n+    # cmd.exe and can flash a console before the user invokes any browser tool.\n+    # Actual browser execution paths still validate the candidate before use.\n     try:\n-        browser_cmd = _find_agent_browser()\n+        browser_cmd = _find_agent_browser(validate=False)\n     except FileNotFoundError:\n         return False\n "]},{"file":"tui_gateway/server.py","hunks":["@@ -278,6 +278,8 @@ def __init__(self, session_key: str, model: str):\n             argv += [\"--model\", model]\n \n         self._closed = False\n+        from hermes_cli._subprocess_compat import windows_hide_flags\n+\n         self.proc = subprocess.Popen(\n             argv,\n             stdin=subprocess.PIPE,","@@ -289,6 +291,7 @@ def __init__(self, session_key: str, model: str):\n             # slash_worker runs the Hermes agent → needs provider credentials.\n             # Tier-1 secrets (gateway/GitHub/infra) are still stripped (#29157).\n             env=hermes_subprocess_env(inherit_credentials=[redacted:secret-assignment]\n+            creationflags=windows_hide_flags(),\n         )\n         threading.Thread(target=self._drain_stdout, daemon=True).start()\n         threading.Thread(target=self._drain_stderr, daemon=True).start()\n"]}]},{"kind":"pr","number":55613,"title":"fix(cli): add CREATE_BREAKAWAY_FROM_JOB to windows_hide_flags()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"0dbf639bc8","number":0},{"kind":"commit","value":"aa2ae36c3f","number":0},{"kind":"pr","value":"#70205","number":70205}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":55667,"title":"fix(cli): add CREATE_BREAKAWAY_FROM_JOB to windows_hide_flags()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#55613","number":55613},{"kind":"pr","value":"#55604","number":55604}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":55692,"title":"fix(cli): include breakaway flag for hidden Windows subprocesses","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#55613","number":55613},{"kind":"pr","value":"#55604","number":55604},{"kind":"pr","value":"#55667","number":55667}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":55753,"title":"fix(windows): add CREATE_BREAKAWAY_FROM_JOB to windows_hide_flags()","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#55613","number":55613},{"kind":"pr","value":"#55604","number":55604},{"kind":"pr","value":"#55667","number":55667},{"kind":"pr","value":"#55692","number":55692}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":64081,"title":"fix: add CREATE_NO_WINDOW to env_probe and lazy_deps subprocess calls on Windows","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"aa2ae36c3fcb6ea9af76c603d262389d832a24dd","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":75224,"title":"fix: hide WhatsApp adapter helper subprocesses on Windows","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":108309,"title":"fix(windows): add CREATE_BREAKAWAY_FROM_JOB to windows_hide_flags()","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}