Windows subprocess Unicode encoding errors and fixes

NousResearch/hermes-agent · cluster 17049 · acp

Data as of

Nothing open on GitHub. Fix merged in #17435. Check it covers your case.

The short answer

0open issues
0open PRs
47items grouped
Known?
Yes. 47 related items grouped here, 18 issues already closed.
Fix event
A fix was merged to main in PR #17435. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PRs #17435, #38334, #70850, #70875. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and pull requests concern UnicodeDecodeError crashes on Windows when subprocess output uses non-UTF-8 locale encodings (e.g., cp1251, GBK). The members propose various fixes including explicit UTF-8 encoding, errors='replace' fallbacks, and environment variable overrides across multiple components.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #17435 fix(windows): decode wmic output with explicit encoding to survive non-UTF-8 locales (#17049)

    merged to main

    files: hermes_cli/gateway.py, hermes_cli/main.py, scripts/release.py, tests/hermes_cli/test_update_stale_dashboard.py

    Show the diff
    • hermes_cli/gateway.py
      @@ -279,9 +279,11 @@ def _matches_current_profile(command: str) -> bool:
                       ["wmic", "process", "get", "ProcessId,CommandLine", "/FORMAT:LIST"],
                       capture_output=True,
                       text=True,
      +                encoding="utf-8",
      +                errors="ignore",
                       timeout=10,
                   )
      -            if result.returncode != 0:
      +            if result.returncode != 0 or result.stdout is None:
                       return []
                   current_cmd = ""
                   for line in result.stdout.split("\n"):
    • hermes_cli/main.py
      @@ -5274,12 +5274,20 @@ def _warn_stale_dashboard_processes() -> None:
       
           try:
               if sys.platform == "win32":
      +            # wmic may emit text in the system code page (for example cp936
      +            # on zh-CN systems), not UTF-8. In text mode, subprocess output
      +            # decoding depends on Python's configuration (locale-dependent
      +            # by default, or UTF-8 in UTF-8 mode). The important protection
      +            # here is errors="ignore": it prevents a reader-thread
      +            # UnicodeDecodeError from leaving result.stdout=None and turning
      +            # the later .split() into an AttributeError (#17049).
                   result = subprocess.run(
                       ["wmic", "process", "get", "ProcessId,CommandLine",
                        "/FORMAT:LIST"],
                       capture_output=True, text=True, timeout=10,
      +                encoding="utf-8", errors="ignore",
                   )
      -            if result.returncode != 0:
      +            if result.returncode != 0 or result.stdout is None:
                       return
                   current_cmd = ""
                   for line in result.stdout.split("\n"):
    • scripts/release.py
      @@ -263,6 +263,7 @@
           "[redacted:email]": "Bartok9",
           "[redacted:email]": "CruxExperts",
           "[redacted:email]": "LeonSGP43",
      +    "[redacted:email]": "Kailigithub",
           "[redacted:email]": "vlwkaos",
           "[redacted:email]": "Tranquil-Flow",
           "[redacted:email]": "mcndjxlefnd",
    • tests/hermes_cli/test_update_stale_dashboard.py
      @@ -175,3 +175,55 @@ def test_grep_lines_ignored(self, capsys):
               output = capsys.readouterr().out
               assert "PID 99999" not in output
               assert "PID 12345" in output
      +
      +
      +class TestWindowsWmicEncoding:
      +    """Regression tests for #17049 — the Windows wmic branch must not crash
      +    `hermes update` on non-UTF-8 system locales (e.g. cp936 on zh-CN).
      +    """
      +
      +    def test_wmic_invoked_with_utf8_ignore_errors(self, monkeypatch):
      +        """The wmic subprocess.run call must pass encoding='utf-8' and
      +        errors='ignore' so the subprocess reader thread cannot raise
      +        UnicodeDecodeError on non-UTF-8 wmic output."""
      +        monkeypatch.setattr(sys, "platform", "win32")
      +        with patch("subprocess.run") as mock_run:
      +            # Provide a minimal valid wmic /FORMAT:LIST response.
      +            mock_run.return_value = MagicMock(
      +                returncode=0,
      +                stdout=(
      +                    "CommandLine=python -m hermes_cli.main dashboard\n"
      +                    "ProcessId=12345\n"
      +                ),
      +                stderr="",
      +            )
      +            _warn_stale_dashboard_processes()
      +
      +        assert mock_run.called, "subprocess.run was not invoked"
      +        kwargs = mock_run.call_args.kwargs
      +        assert kwargs.get("encoding") == "utf-8", (
      +            "encoding kwarg must be 'utf-8' so wmic output is decoded "
      +            "deterministically rather than via the implicit reader-thread "
      +            "default that crashes on non-UTF-8 locales (#17049)."
      +        )
      +        assert kwargs.get("errors") == "ignore", (
      +            "errors kwarg must be 'ignore' so undecodable bytes don't take "
      +            "down the reader thread (#17049)."
      +        )
      +
      +    def test_wmic_returns_none_stdout_does_not_crash(self, monkeypatch, capsys):
      +        """If subprocess.run returns successfully but stdout is None — which
      +        is what Python 3.11 leaves behind when the reader thread silently
      +        crashed on UnicodeDecodeError before this fix landed — the warning
      +        must short-circuit instead of raising AttributeError on
      +        ``None.split('\\n')`` and aborting `hermes update` (#17049)."""
      +        monkeypatch.setattr(sys, "platform", "win32")
      +        with patch("subprocess.run") as mock_run:
      +            mock_run.return_value = MagicMock(
      +                returncode=0, stdout=None, stderr=""
      +            )
      +            # Must not raise.
      +            _warn_stale_dashboard_processes()
      +
      +        output = capsys.readouterr().out
      +        assert "dashboard process" not in output
  • #38334 fix(gateway): decode schtasks output with locale encoding on Windows (#38172)

    merged to main

    files: hermes_cli/gateway_windows.py, tests/hermes_cli/test_gateway_windows.py

    Show the diff
    • hermes_cli/gateway_windows.py
      @@ -29,6 +29,7 @@
       from __future__ import annotations
       
       import ctypes
      +import locale
       import os
       import re
       import shlex
      @@ -52,6 +53,20 @@
       _TASK_DESCRIPTION = "Hermes Agent Gateway - Messaging Platform Integration"
       
       
      +def _schtasks_encoding() -> str:
      +    """Best-effort console encoding for decoding ``schtasks.exe`` output.
      +
      +    On localized Windows (e.g. Chinese), ``schtasks`` emits text in the OEM/ANSI
      +    code page rather than UTF-8. Decoding with the wrong codec raised
      +    ``UnicodeDecodeError`` inside ``subprocess``' reader threads. Prefer the
      +    locale's preferred encoding and fall back to UTF-8.
      +    """
      +    try:
      +        return locale.getpreferredencoding(False) or "utf-8"
      +    except Exception:
      +        return "utf-8"
      +
      +
       # ---------------------------------------------------------------------------
       # Platform guard
       # ---------------------------------------------------------------------------
      @@ -112,6 +127,12 @@ def _exec_schtasks(args: list[str]) -> tuple[int, str, str]:
                   [schtasks, *args],
                   capture_output=True,
                   text=True,
      +            # Localized Windows emits schtasks output in the console code page,
      +            # not UTF-8. Decode with the locale encoding and replace undecodable
      +            # bytes so a non-UTF-8 status line never surfaces a UnicodeDecodeError
      +            # traceback from subprocess' reader threads (issue #38172).
      +            encoding=_schtasks_encoding(),
      +            errors="replace",
                   timeout=_SCHTASKS_TIMEOUT_S,
                   # CREATE_NO_WINDOW avoids a flashing console window when the CLI
                   # is itself hosted in a TUI. See tools/browser_tool.py for the
    • tests/hermes_cli/test_gateway_windows.py
      @@ -29,6 +29,49 @@ def test_schtasks_fallback_does_not_hide_unknown_errors():
           assert gateway_windows._should_fall_back(1, "ERROR: The system cannot find the file specified.") is False
       
       
      +def test_schtasks_encoding_falls_back_to_utf8(monkeypatch):
      +    """A broken/empty locale must not leave us without a decoder (issue #38172)."""
      +
      +    monkeypatch.setattr(gateway_windows.locale, "getpreferredencoding", lambda *a, **k: "")
      +    assert gateway_windows._schtasks_encoding() == "utf-8"
      +
      +    def _boom(*args, **kwargs):
      +        raise RuntimeError("locale exploded")
      +
      +    monkeypatch.setattr(gateway_windows.locale, "getpreferredencoding", _boom)
      +    assert gateway_windows._schtasks_encoding() == "utf-8"
      +
      +
      +def test_exec_schtasks_decodes_with_replace_errors(monkeypatch):
      +    """schtasks output must be decoded with errors='replace' so localized
      +    (non-UTF-8) bytes never surface a UnicodeDecodeError traceback (#38172)."""
      +
      +    captured: dict[str, object] = {}
      +
      +    class _FakeCompleted:
      +        returncode = 0
      +        stdout = "ok"
      +        stderr = ""
      +
      +    def fake_run(cmd, **kwargs):
      +        captured["cmd"] = cmd
      +        captured.update(kwargs)
      +        return _FakeCompleted()
      +
      +    monkeypatch.setattr(gateway_windows, "_assert_windows", lambda: None)
      +    monkeypatch.setattr(gateway_windows.shutil, "which", lambda name: r"C:\\Windows\\System32\\schtasks.exe")
      +    monkeypatch.setattr(gateway_windows.subprocess, "run", fake_run)
      +
      +    code, out, err = gateway_windows._exec_schtasks(["/Query", "/TN", "Hermes_Gateway"])
      +
      +    assert (code, out, err) == (0, "ok", "")
      +    assert captured["errors"] == "replace", "schtasks output must decode with errors='replace'"
      +    assert isinstance(captured["encoding"], str) and captured["encoding"], (
      +        "an explicit non-empty encoding must be passed to subprocess.run"
      +    )
      +    assert captured["text"] is True
      +
      +
       def test_build_gateway_argv_uses_base_pythonw_for_uv_venv_launcher(monkeypatch, tmp_path):
           """Avoid uv's venv pythonw launcher because it respawns console python.exe."""
       
  • #70850 fix(windows): utf-8 subprocess decode across the desktop backend — salvage #61978 + widen to sibling sites

    merged to main

    files: hermes_cli/banner.py, hermes_cli/web_server.py, tests/tui_gateway/test_subprocess_encoding.py, tui_gateway/compute_host.py, tui_gateway/host_supervisor.py, tui_gateway/server.py

  • #70875 fix(windows): close out the text-mode subprocess decode bug class — salvage #55339/#60741/#60751 + full sweep + linter

    merged to main

    files: agent/anthropic_adapter.py, agent/context_references.py, agent/copilot_acp_client.py, agent/lsp/install.py, agent/proxy_sources/iron_proxy.py, agent/secret_sources/base.py, agent/secret_sources/bitwarden.py, agent/shell_hooks.py, agent/skill_preprocessing.py, agent/trace_upload.py, agent/transports/codex_app_server.py, batch_runner.py, cli.py, contributors/emails/[redacted:email], contributors/emails/[redacted:email], gateway/platforms/base.py, gateway/platforms/webhook.py, gateway/platforms/webhook_filters.py, gateway/run.py, gateway/shutdown_forensics.py (+72 more)

  • #71014 fix(windows): residual encoding gaps — MCP stdio, gateway update file-I/O, STT/TTS, desktop spawn, bootstrap ver-stub

    merged to main

    files: apps/desktop/electron/backend-env.test.ts, apps/desktop/electron/backend-env.ts, gateway/dead_targets.py, gateway/delivery.py, gateway/platforms/qqbot/adapter.py, gateway/run.py, gateway/slash_commands.py, gateway/status.py, hermes_bootstrap.py, plugins/platforms/discord/adapter.py, plugins/platforms/feishu/adapter.py, plugins/platforms/google_chat/adapter.py, plugins/platforms/google_chat/oauth.py, plugins/platforms/telegram/adapter.py, plugins/platforms/whatsapp/adapter.py, tests/gateway/test_gateway_utf8_encoding.py, tests/test_hermes_bootstrap.py, tests/tools/test_mcp_stdio_encoding_handler.py, tools/mcp_tool.py, tools/transcription_tools.py (+1 more)

24 PRs closed without merging

From the close comments: merged via another change 7 · superseded by another PR 6 · done elsewhere 5 · duplicate 2 · other 4

  • #17073 fix(gateway): handle wmic UnicodeDecodeError gracefully · duplicate → #17074, #17049
  • #17074 Fix UnicodeDecodeError in Windows wmic process scanning #17049 · done elsewhere → cf83982, #17049
  • #17123 fix(update): protect dashboard wmic scan against UnicodeDecodeError on Windows non-UTF-8 locales (#17049) · merged via another change → #17435
  • #17252 fix(gateway): handle wmic encoding errors on Windows non-English locales · merged via another change → #17435
  • #17271 fix(gateway): add errors='replace' to subprocess.run for Windows wmic compatibility · superseded by another PR → #17435, #17049
Show 19 more
  • #38186 fix(gateway): decode schtasks output with locale encoding on Windows (#38172) · merged via another change → 973decc, df848bd, #38334, #38172
  • #38187 fix(cli/windows): decode schtasks output with locale encoding to silence UnicodeDecodeError (#38172) · done elsewhere → 973decc, #38172
  • #39539 fix(windows): tolerate localized taskkill decode errors · other → 0f732cb, #70875, #55339, #60741, #60751
  • #45099 fix(tools): add errors='replace' to subprocess. · merged via another change → #71014, #70875
  • #45144 fix(windows): set UTF-8 encoding for Copilot ACP subprocess · done elsewhere → #45099
  • #47951 fix(environments): pass encoding='utf-8' errors='replace' to all text-mode Popen calls · superseded by another PR → #45099
  • #47993 fix(subprocess): add UTF-8 encoding to _popen_bash for Windows (#47939) · other
  • #48838 fix(cli): tolerate undecodable alias lookup output · superseded by another PR → c89481d, #53428
  • #48839 fix(cli): default Windows subprocess text decoding · duplicate → #48838
  • #49510 fix(doctor): decode subprocess output as UTF-8 on non-UTF-8 (GBK) locales · done elsewhere → 38b67a3
  • #49512 fix(doctor): pin utf-8/replace decoding for captured subprocesses · superseded by another PR → #49510, #49499
  • #49564 fix(cli): add encoding to subprocess calls in doctor (#49499) · other
  • #53460 fix: add encoding=utf-8, errors=replace to all subprocess text=True calls (#53428) · superseded by another PR → #54220, #54236, #53892, #54417
  • #53549 fix: add encoding='utf-8' to subprocess calls with text=True · superseded by another PR → #54220, #54236, #53892, #54417
  • #55339 fix: add explicit UTF-8 encoding to all subprocess text=True calls (#53428) · merged via another change → #70875, #60751
  • #60832 fix(windows): force utf8 for detached gateway · done elsewhere → #70850, #70875, #71014
  • #61978 fix: harden tui_gateway subprocess reads against Windows locale UnicodeDecodeError (#53137) · merged via another change → #70850
  • #63716 fix(tui): decode slash worker streams as UTF-8 · merged via another change → ead23aa, #70850, #52700, #61978
  • #68546 fix(doctor): decode subprocess output as utf-8 · other → 0f732cb, #70875, #55339, #60741, #60751
Closed issues · 18
  • Closed#17049[Bug]: UnicodeDecodeError and AttributeError in Windows gateway process scanning
  • Closed#31420[Bug] Windows: subprocess env missing PYTHONUTF8=1 causes UnicodeEncodeError on non-ASCII output in CP936/legacy locales
  • Closed#38172Windows: hermes gateway status emits UnicodeDecodeError on localized schtasks output
  • Closed#39029Windows: UnicodeDecodeError in subprocess._readerthread — cp1251 can't decode byte 0x98
  • Closed#45140[Bug]: UnicodeDecodeError in copilot_acp_client.py _stdout_reader on Windows
  • Closed#47939[Bug]: BUG_WINDOWS_UNICODE_SUBPROCESS
  • Closed#49491UnicodeDecodeError-on-Windows-doctor
  • Closed#49492Bug-Windows-UnicodeDecodeError-doctor
  • Closed#49496hermes doctor crashes with UnicodeDecodeError on Chinese Windows GBK locale
  • Closed#49499hermes doctor crashes with UnicodeDecodeError on Chinese Windows GBK locale
  • Closed#52649Windows: Gateway crashes with UnicodeDecodeError (cp950) in _readerthread — causes zombie cycle every ~580s
  • Closed#53137fix: TUI gateway crash on Windows - UnicodeDecodeError in subprocess _readerthread
  • Closed#53428subprocess.run(text=True) without encoding param triggers GBK crash on Chinese Windows (21 locations)
  • Closed#55020Windows Chinese locale (GBK) causes subprocess._readerthread UnicodeDecodeError crash
  • Closed#57238Bug: GBK encoding crash on Chinese Windows[Bug] GBK encoding crash in terminal tool on Chinese Windows (cp936)
  • Closed#60760[Windows] Detached gateway crashes with `UnicodeDecodeError: 'gbk'` when a spawned child process emits UTF-8 output
  • Closed#61595[Windows] subprocess._readerthread crashes with UnicodeDecodeError on GBK output
  • Closed#63686Windows CJK locale: UnicodeDecodeError in _SlashWorker crashes gateway (text=True defaults to GBK)
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.05: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means 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 Lab

At a glance

Subsystem
acp
Merged PRs
#17435, #38334, #70850, #70875, #71014
Closed issues
18
Attempts
24 PRs closed without merging
Fix link checked
24 Jul 2026

For your agent

The same facts, typed.