From f106fcd1f56a93f53e4420d662aceda7236c6d6e Mon Sep 17 00:00:00 2001 From: Jinho Seok Date: Tue, 7 Jul 2026 01:50:38 +0000 Subject: [PATCH] fix(workspace-git): hide git subprocess console windows on Windows (#5692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add creationflags to _run_git() and _config_names_for_scope() so git child processes don't accumulate hidden cmd.exe/conhost windows on Windows. Uses a local module-level _windows_hide_flags() helper (mirrors the api/updates.py pattern: CREATE_NO_WINDOW on win32, 0 elsewhere) rather than importing from the OPTIONAL hermes_cli package — a standalone/agent-less WebUI (and the repo's own CI runners, which don't pip-install hermes-agent) must keep full workspace-git functionality. windows_hide_flags() is a safe no-op (0) on non-Windows. Scoped to workspace git commands. Adds mutation-checked tests asserting the kwarg is wired on both sites. Co-authored-by: nesquena-hermes --- api/workspace_git.py | 17 +++++++++++ tests/test_workspace_git.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/api/workspace_git.py b/api/workspace_git.py index a33fd316..457d8720 100644 --- a/api/workspace_git.py +++ b/api/workspace_git.py @@ -12,6 +12,7 @@ import logging import os import shutil import subprocess +import sys import tempfile import threading import re @@ -22,6 +23,20 @@ from typing import Iterable logger = logging.getLogger(__name__) + +def _windows_hide_flags() -> int: + """Win32 ``creationflags`` that hide a short-lived console child's window + (``CREATE_NO_WINDOW``) without detaching it, so ``capture_output`` still + works. Returns ``0`` on non-Windows — the ``subprocess`` default, a genuine + no-op. Mirrors the ``api/updates.py`` pattern; kept local so workspace-git + never takes a hard dependency on the optional ``hermes_cli`` package (a + standalone/agent-less WebUI must keep full git functionality). See #5692. + """ + if sys.platform == "win32": + return getattr(subprocess, "CREATE_NO_WINDOW", 0) + return 0 + + from api.workspace import rmtree_anchored, safe_resolve_ws, unlink_anchored @@ -248,6 +263,7 @@ def _run_git( text=True, timeout=timeout, env=run_env, + creationflags=_windows_hide_flags(), ) except subprocess.TimeoutExpired as exc: raise GitWorkspaceError("Git command timed out", "timeout") from exc @@ -290,6 +306,7 @@ def _config_names_for_scope( capture_output=True, timeout=GIT_TIMEOUT, env=env, + creationflags=_windows_hide_flags(), ) if result.returncode not in {0, 1}: if ignore_unsupported: diff --git a/tests/test_workspace_git.py b/tests/test_workspace_git.py index f9e28ac3..98e0838d 100644 --- a/tests/test_workspace_git.py +++ b/tests/test_workspace_git.py @@ -2515,3 +2515,61 @@ def test_dirty_worktree_uses_filter_neutralization(tmp_path): _dirty_worktree(ctx) assert not marker.exists() + + +def test_run_git_passes_windows_hide_flags(monkeypatch, tmp_path): + """_run_git must pass creationflags=_windows_hide_flags() so git child + processes don't accumulate visible console windows on Windows (#5692). + windows_hide_flags() is 0 on non-Windows, so this is a safe no-op there; + the test asserts the kwarg is wired regardless of platform.""" + import api.workspace_git as wg + from api.workspace_git import _windows_hide_flags + + repo = _init_repo(tmp_path / "repo") + (repo / "f.txt").write_text("x\n", encoding="utf-8") + _commit_all(repo) + + captured = {} + real_run = subprocess.run + + def fake_run(*args, **kwargs): + captured["creationflags"] = kwargs.get("creationflags", "MISSING") + return real_run(*args, **kwargs) + + monkeypatch.setattr(wg.subprocess, "run", fake_run) + wg._run_git(repo, ["rev-parse", "HEAD"]) + + assert captured.get("creationflags") == _windows_hide_flags(), ( + "_run_git must pass creationflags=_windows_hide_flags() to subprocess.run" + ) + + +def test_config_names_for_scope_passes_windows_hide_flags(monkeypatch, tmp_path): + """_config_names_for_scope must also pass creationflags=_windows_hide_flags() + (#5692) — the git-config probe spawns a console window on Windows too.""" + import re as _re + import api.workspace_git as wg + from api.workspace_git import _windows_hide_flags + + repo = _init_repo(tmp_path / "repo") + + captured = {} + real_run = subprocess.run + + def fake_run(*args, **kwargs): + captured["creationflags"] = kwargs.get("creationflags", "MISSING") + return real_run(*args, **kwargs) + + monkeypatch.setattr(wg.subprocess, "run", fake_run) + wg._config_names_for_scope( + "--local", + repo, + {}, + "filter\\..*", + _re.compile(r"^filter\."), + ignore_unsupported=True, + ) + + assert captured.get("creationflags") == _windows_hide_flags(), ( + "_config_names_for_scope must pass creationflags=_windows_hide_flags()" + )