Windows console flash and encoding fixes for git probes
Cluster 53178 · dashboard. Data as of
Nothing open on GitHub. Fix merged in #54236 on 28 Jun 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #54236. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items address two related Windows-specific bugs: subprocess calls spawning visible console windows during git/shell operations, and a cp950 encoding crash on non-ASCII paths in the dashboard's git probe logic.
Open issues0
Open PRs0
Fixes on record
-
#54236 fix(windows): hide console-window flash on backend git/gh/wmic/bash subprocess spawns
files:
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.pyShow the diff
-
agent/coding_context.py@@ -60,6 +60,8 @@ from pathlib import Path from typing import Any, Optional +from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags + logger = logging.getLogger("hermes.coding_context") CODING_TOOLSET = "coding" @@ -647,12 +649,14 @@ def _enabled_mcp_servers(config: Optional[dict[str, Any]]) -> list[str]: def _git(cwd: Path, *args: str) -> str: + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} try: out = subprocess.run( ["git", "-C", str(cwd), *args], capture_output=True, text=True, timeout=_GIT_TIMEOUT, + **_popen_kwargs, ) except (OSError, subprocess.SubprocessError): return "" -
agent/context_references.py@@ -12,6 +12,7 @@ from typing import Awaitable, Callable from agent.model_metadata import estimate_tokens_rough +from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags _QUOTED_REFERENCE_VALUE = r'(?:`[^`\n]+`|"[^"\n]+"|\'[^\'\n]+\')' REFERENCE_PATTERN = re.compile( @@ -290,6 +291,7 @@ def _expand_git_reference( args: list[str], label: str, ) -> tuple[str | None, str | None]: + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} try: result = subprocess.run( ["git", *args], @@ -298,6 +300,7 @@ def _expand_git_reference( text=True, timeout=30, stdin=subprocess.DEVNULL, + **_popen_kwargs, ) except subprocess.TimeoutExpired: 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]: def _rg_files(path: Path, cwd: Path, limit: int) -> list[Path] | None: + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} try: result = subprocess.run( ["rg", "--files", str(path.relative_to(cwd))], @@ -491,6 +495,7 @@ def _rg_files(path: Path, cwd: Path, limit: int) -> list[Path] | None: text=True, timeout=10, stdin=subprocess.DEVNULL, + **_popen_kwargs, ) except (FileNotFoundError, OSError, subprocess.TimeoutExpired): return None -
agent/shell_hooks.py@@ -122,6 +122,8 @@ from pathlib import Path from typing import Any, Callable, Dict, Iterator, List, Optional, Set, Tuple +from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags + try: import fcntl # POSIX only; Windows falls back to best-effort without flock. except ImportError: # pragma: no cover @@ -441,6 +443,7 @@ def _spawn(spec: ShellHookSpec, stdin_json: str) -> Dict[str, Any]: return result t0 = time.monotonic() + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} try: proc = subprocess.run( argv, @@ -449,6 +452,7 @@ def _spawn(spec: ShellHookSpec, stdin_json: str) -> Dict[str, Any]: timeout=spec.timeout, text=True, shell=False, + **_popen_kwargs, ) except subprocess.TimeoutExpired: result["timed_out"] = True -
agent/skill_preprocessing.py@@ -5,6 +5,8 @@ import subprocess from pathlib import Path +from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags + logger = logging.getLogger(__name__) # 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: Failures return a short ``[inline-shell error: ...]`` marker instead of raising, so one bad snippet can't wreck the whole skill message. """ + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} try: completed = subprocess.run( ["bash", "-c", command], @@ -75,6 +78,7 @@ def run_inline_shell(command: str, cwd: Path | None, timeout: int) -> str: timeout=max(1, int(timeout)), check=False, stdin=subprocess.DEVNULL, + **_popen_kwargs, ) except subprocess.TimeoutExpired: return f"[inline-shell timeout after {timeout}s: {command}]" -
gateway/status.py@@ -81,12 +81,18 @@ def terminate_pid(pid: int, *, force: bool = False) -> None: because os.kill(..., SIGTERM) is not equivalent to a tree-killing hard stop. """ if force and _IS_WINDOWS: + # CREATE_NO_WINDOW: terminate_pid runs from the windowless pythonw.exe + # gateway/desktop backend, so a bare taskkill spawn would flash a + # conhost window on every force-kill. + from hermes_cli._subprocess_compat import windows_hide_flags + try: result = subprocess.run( ["taskkill", "/PID", str(pid), "/T", "/F"], capture_output=True, text=True, timeout=10, + creationflags=windows_hide_flags(), ) except FileNotFoundError: os.kill(pid, signal.SIGTERM) -
hermes_cli/copilot_auth.py@@ -27,6 +27,8 @@ from pathlib import Path from typing import Optional +from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags + logger = logging.getLogger(__name__) # OAuth device code flow constants (same client ID as opencode/Copilot CLI) @@ -130,6 +132,7 @@ def _try_gh_cli_token() -> Optional[str]: clean_env = {k: v for k, v in os.environ.items() if k not in {"GITHUB_TOKEN", "GH_TOKEN"}} + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} for gh_path in _gh_cli_candidates(): cmd = [gh_path, "auth", "token"] if hostname: @@ -141,6 +144,7 @@ def _try_gh_cli_token() -> Optional[str]: text=True, timeout=5, env=clean_env, + **_popen_kwargs, ) except (FileNotFoundError, subprocess.TimeoutExpired) as exc: logger.debug("gh CLI token lookup failed (%s): %s", gh_path, exc) -
hermes_cli/gateway.py@@ -384,6 +384,12 @@ def _matches_gateway_runtime(command: str) -> bool: # removed as part of the WMIC deprecation — fall back to # PowerShell's Get-CimInstance. Any OSError here (FileNotFoundError # on missing wmic) trips the fallback. + # Hide the console window: this scan runs inside the windowless + # pythonw.exe gateway/desktop backend, so a bare wmic/powershell + # spawn would flash a conhost window on every watchdog probe. + from hermes_cli._subprocess_compat import windows_hide_flags + + _no_window = {"creationflags": windows_hide_flags()} wmic_path = shutil.which("wmic") used_fallback = False result = None @@ -402,6 +408,7 @@ def _matches_gateway_runtime(command: str) -> bool: encoding="utf-8", errors="ignore", timeout=10, + **_no_window, ) except (OSError, subprocess.TimeoutExpired): result = None @@ -427,6 +434,7 @@ def _matches_gateway_runtime(command: str) -> bool: encoding="utf-8", errors="ignore", timeout=15, + **_no_window, ) except (OSError, subprocess.TimeoutExpired): return [] -
hermes_cli/main.py@@ -5825,6 +5825,11 @@ def _find_stale_dashboard_pids( # here is errors="ignore": it prevents a reader-thread # UnicodeDecodeError from leaving result.stdout=None and turning # the later .split() into an AttributeError (#17049). + # CREATE_NO_WINDOW hides the conhost flash: this scan can run from + # the windowless pythonw.exe desktop/gateway backend during an + # update, where a bare wmic spawn would pop a console window. + from hermes_cli._subprocess_compat import windows_hide_flags + result = subprocess.run( ["wmic", "process", "get", "ProcessId,CommandLine", "/FORMAT:LIST"], capture_output=True, @@ -5832,6 +5837,7 @@ def _find_stale_dashboard_pids( timeout=10, encoding="utf-8", errors="ignore", + creationflags=windows_hide_flags(), ) if result.returncode != 0 or result.stdout is None: return [] -
tests/gateway/test_status.py@@ -621,16 +621,22 @@ def test_force_uses_taskkill_on_windows(self, monkeypatch): calls = [] monkeypatch.setattr(status, "_IS_WINDOWS", True) - def fake_run(cmd, capture_output=False, text=False, timeout=None): - calls.append((cmd, capture_output, text, timeout)) + def fake_run(cmd, capture_output=False, text=False, timeout=None, creationflags=0): + calls.append((cmd, capture_output, text, timeout, creationflags)) return SimpleNamespace(returncode=0, stdout="", stderr="") monkeypatch.setattr(status.subprocess, "run", fake_run) status.terminate_pid(123, force=True) + # taskkill is spawned with the no-window flag so the windowless + # pythonw.exe backend doesn't flash a conhost window on force-kill. + # windows_hide_flags() is 0 on the POSIX test host (a valid no-op + # creationflags value); on real Windows it is CREATE_NO_WINDOW. + from hermes_cli._subprocess_compat import windows_hide_flags + assert calls == [ - (["taskkill", "/PID", "123", "/T", "/F"], True, True, 10) + (["taskkill", "/PID", "123", "/T", "/F"], True, True, 10, windows_hide_flags()) ] def test_force_falls_back_to_sigterm_when_taskkill_missing(self, monkeypatch): -
tests/test_windows_subprocess_no_window_flags.py@@ -0,0 +1,263 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path +from types import SimpleNamespace + + +_CREATE_NO_WINDOW = 0x08000000 + + +class _Completed: + def __init__(self, stdout: str | bytes = "ok\n", returncode: int = 0): + self.stdout = stdout + self.stderr = "" + self.returncode = returncode + + +def test_tui_gateway_git_probe_hides_git_windows(monkeypatch): + from tui_gateway import git_probe + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return _Completed(stdout="main\n") + + monkeypatch.setattr(git_probe, "IS_WINDOWS", True) + monkeypatch.setattr(git_probe, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(git_probe.subprocess, "run", fake_run) + + assert git_probe.run_git("C:/repo", "branch", "--show-current") == "main" + + assert captured == [ + ( + ["git", "-C", "C:/repo", "branch", "--show-current"], + { + "capture_output": True, + "text": True, + "encoding": "utf-8", + "errors": "replace", + "timeout": git_probe._GIT_TIMEOUT, + "check": False, + "stdin": subprocess.DEVNULL, + "creationflags": _CREATE_NO_WINDOW, + }, + ) + ] + + +def test_tui_gateway_fuzzy_file_listing_hides_git_windows(monkeypatch): + from hermes_cli import _subprocess_compat + from tui_gateway import server + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + if cmd[-1] == "--show-toplevel": + return _Completed(stdout=b"C:/repo\n") + return _Completed(stdout=b"src/main.py\0README.md\0") + + monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True) + monkeypatch.setattr(_subprocess_compat, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(server.subprocess, "run", fake_run) + server._fuzzy_cache.clear() + + assert server._list_repo_files("C:/repo") == ["src/main.py", "README.md"] + + assert [kwargs["creationflags"] for _, kwargs in captured] == [ + _CREATE_NO_WINDOW, + _CREATE_NO_WINDOW, + ] + + +def test_coding_context_git_hides_git_windows(monkeypatch): + from agent import coding_context + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return _Completed(stdout="clean\n") + + monkeypatch.setattr(coding_context, "IS_WINDOWS", True) + monkeypatch.setattr(coding_context, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(coding_context.subprocess, "run", fake_run) + + assert coding_context._git(Path("C:/repo"), "status", "--short") == "clean" + assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW + + +def test_context_reference_git_and_rg_hide_windows(monkeypatch): + from agent import context_references + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + if cmd[0] == "rg": + return _Completed(stdout="src/main.py\n") + return _Completed(stdout="diff --git a/src/main.py b/src/main.py\n") + + monkeypatch.setattr(context_references, "IS_WINDOWS", True) + monkeypatch.setattr(context_references, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(context_references.subprocess, "run", fake_run) + + ref = context_references.ContextReference( + raw="@diff", + kind="diff", + target="", + start=0, + end=5, + ) + warning, block = context_references._expand_git_reference( + ref, + Path("C:/repo"), + ["diff"], + "git diff", + ) + assert warning is None + assert block is not None + assert "git diff" in block + assert context_references._rg_files(Path("C:/repo/src"), Path("C:/repo"), 10) == [ + Path("src/main.py") + ] + + assert [kwargs["creationflags"] for _, kwargs in captured] == [ + _CREATE_NO_WINDOW, + _CREATE_NO_WINDOW, + ] + + +def test_copilot_gh_cli_probe_hides_gh_windows(monkeypatch): + from hermes_cli import copilot_auth + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return _Completed(stdout="gho_from_cli\n") + + monkeypatch.setattr(copilot_auth, "IS_WINDOWS", True) + monkeypatch.setattr(copilot_auth, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(copilot_auth, "_gh_cli_candidates", lambda: ["gh"]) + monkeypatch.setattr(copilot_auth.subprocess, "run", fake_run) + + assert copilot_auth._try_gh_cli_token() == "gho_from_cli" + assert captured[0][0] == ["gh", "auth", "token"] + assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW + + +def test_gateway_pid_scan_hides_wmic_and_powershell_windows(monkeypatch): + from hermes_cli import gateway + from hermes_cli import _subprocess_compat + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + if cmd[0] == "wmic": + return _Completed(stdout="", returncode=1) + return _Completed(stdout="CommandLine=hermes gateway\nProcessId=123\n") + + monkeypatch.setattr(gateway, "is_windows", lambda: True) + monkeypatch.setattr(gateway.shutil, "which", lambda name: name) + monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True) + monkeypatch.setattr(_subprocess_compat, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(gateway.subprocess, "run", fake_run) + + assert gateway._scan_gateway_pids(set()) == [123] + assert [kwargs["creationflags"] for _, kwargs in captured] == [ + _CREATE_NO_WINDOW, + _CREATE_NO_WINDOW, + ] + + +def test_stale_dashboard_windows_scan_hides_wmic(monkeypatch): + from hermes_cli import main + from hermes_cli import _subprocess_compat + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return _Completed(stdout="CommandLine=hermes dashboard\nProcessId=123\n") + + monkeypatch.setattr(main.sys, "platform", "win32") + monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True) + monkeypatch.setattr(_subprocess_compat, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(main.subprocess, "run", fake_run) + + assert main._find_stale_dashboard_pids() == [123] + assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW + + +def test_gateway_force_kill_hides_taskkill_window(monkeypatch): + from gateway import status + from hermes_cli import _subprocess_compat + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return _Completed(stdout="") + + monkeypatch.setattr(status, "_IS_WINDOWS", True) + monkeypatch.setattr(_subprocess_compat, "IS_WINDOWS", True) + monkeypatch.setattr(_subprocess_compat, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(status.subprocess, "run", fake_run) + + status.terminate_pid(123, force=True) + + assert captured == [ + ( + ["taskkill", "/PID", "123", "/T", "/F"], + { + "capture_output": True, + "text": True, + "timeout": 10, + "creationflags": _CREATE_NO_WINDOW, + }, + ) + ] + + +def test_shell_hooks_hide_hook_command_windows(monkeypatch): + from agent import shell_hooks + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return SimpleNamespace(returncode=0, stdout="{}", stderr="") + + monkeypatch.setattr(shell_hooks, "IS_WINDOWS", True) + monkeypatch.setattr(shell_hooks, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(shell_hooks.subprocess, "run", fake_run) + + result = shell_hooks._spawn( + shell_hooks.ShellHookSpec(event="post_tool_call", command="hook-bin --flag"), + "{}", + ) + + assert result["returncode"] == 0 + assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW + + +def test_inline_skill_shell_hides_bash_window(monkeypatch): + from agent import skill_preprocessing + + captured = [] + + def fake_run(cmd, **kwargs): + captured.append((cmd, kwargs)) + return SimpleNamespace(returncode=0, stdout="ok\n", stderr="") + + monkeypatch.setattr(skill_preprocessing, "IS_WINDOWS", True) + monkeypatch.setattr(skill_preprocessing, "windows_hide_flags", lambda: _CREATE_NO_WINDOW) + monkeypatch.setattr(skill_preprocessing.subprocess, "run", fake_run) + + assert skill_preprocessing.run_inline_shell("echo ok", cwd=None, timeout=5) == "ok" + assert captured[0][0] == ["bash", "-c", "echo ok"] + assert captured[0][1]["creationflags"] == _CREATE_NO_WINDOW -
tui_gateway/git_probe.py@@ -31,6 +31,8 @@ from collections.abc import Iterable from concurrent.futures import ThreadPoolExecutor +from hermes_cli._subprocess_compat import IS_WINDOWS, windows_hide_flags + _GIT_TIMEOUT = 1.5 _WARM_WORKERS = 8 @@ -45,14 +47,18 @@ def run_git(cwd: str, *args: str) -> str: """``git -C <cwd> <args>`` → stripped stdout, or ``""`` on any failure.""" if not cwd: return "" + _popen_kwargs = {"creationflags": windows_hide_flags()} if IS_WINDOWS else {} try: result = subprocess.run( ["git", "-C", cwd, *args], capture_output=True, text=True, + encoding="utf-8", + errors="replace", timeout=_GIT_TIMEOUT, check=False, stdin=subprocess.DEVNULL, + **_popen_kwargs, ) return result.stdout.strip() if result.returncode == 0 else "" except Exception: -
tui_gateway/server.py@@ -11668,6 +11668,9 @@ def _list_repo_files(root: str) -> list[str]: return cached[1] files: list[str] = [] + from hermes_cli._subprocess_compat import windows_hide_flags + + _creationflags = windows_hide_flags() try: top_result = subprocess.run( ["git", "-C", root, "rev-parse", "--show-toplevel"], @@ -11675,6 +11678,7 @@ def _list_repo_files(root: str) -> list[str]: timeout=2.0, check=False, stdin=subprocess.DEVNULL, + creationflags=_creationflags, ) if top_result.returncode == 0: top = top_result.stdout.decode("utf-8", "replace").strip() @@ -11693,6 +11697,7 @@ def _list_repo_files(root: str) -> list[str]: timeout=2.0, check=False, stdin=subprocess.DEVNULL, + creationflags=_creationflags, ) if list_result.returncode == 0: for p in list_result.stdout.decode("utf-8", "replace").split("\0"):
-
1 PR closed without merging
From the close comments: superseded by another PR 1
-
#53184 fix(windows): suppress git-probe console flash + cp950 crash in dashboard · superseded by another PR →
#54220,#54236,#53892,#54417
Closed issues · 1
- Closed#53178[Windows] dashboard Projects-tree git probe (tui_gateway/git_probe.py run_git) flashes a console window per repo + crashes on non-ASCII paths (missing CREATE_NO_WINDOW + encoding)
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- dashboard
- Merged PRs
- #54236
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 28 Jun 2026