{"repo":"NousResearch/hermes-agent","public_id":"53539","display_label":"Windows console window flashing from spawned backend processes","summary":"The members all concern console windows flashing on Windows when the desktop app or gateway spawns subprocesses (pythonw, ffmpeg, netstat, restart watcher). One also mentions a WebSocket 1006 disconnect tied to a backend restart loop; the cluster does not establish resolution status.","subsystem":"apps/desktop","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":1},"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_SPANNING","link_density":0.5},"fix_edges":[{"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":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":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":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":53539,"title":"[Desktop] Command windows keep flashing — WebSocket 1006 disconnect causes backend restart loop","url":null,"state":"closed"},{"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":54342,"title":"fix(gateway): use pythonw.exe for restart watcher + add dedup and backoff","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":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"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}