From 17b3b875c8fd64e2cad581c51cf4ad7ef2cf1f7b Mon Sep 17 00:00:00 2001 From: Mohammed Alkindi Date: Fri, 21 Aug 2026 04:03:42 +0400 Subject: [PATCH] fix(sessions): decode git worktree paths as UTF-8 git worktree list --porcelain emits paths as raw UTF-8, but the subprocess call decoded them with the locale default. On a stock Windows install that is cp1252, which misreads a non-ASCII worktree path in one of two ways. If the path's UTF-8 contains 0x81, 0x8D, 0x8F, 0x90 or 0x9D, the five bytes cp1252 leaves undefined, the decode raises UnicodeDecodeError inside subprocess's reader thread. That never reaches the caller, which sees returncode 0 with stdout None, so the existing guard returns an empty list and every worktree in the repo is lost, ASCII ones included. Otherwise the bytes decode to mojibake and the returned path does not resolve on disk. Measured on Windows with a Cyrillic worktree: 0 of 2 entries returned before, 2 of 2 after. --- src/claude_agent_sdk/_internal/sessions.py | 1 + tests/test_sessions.py | 45 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/claude_agent_sdk/_internal/sessions.py b/src/claude_agent_sdk/_internal/sessions.py index 2634a0f37..ea0d28178 100644 --- a/src/claude_agent_sdk/_internal/sessions.py +++ b/src/claude_agent_sdk/_internal/sessions.py @@ -396,6 +396,7 @@ def _get_worktree_paths(cwd: str) -> list[str]: cwd=cwd, capture_output=True, text=True, + encoding="utf-8", timeout=5, check=False, ) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 3e2ffd9a4..a98650e2c 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -4,6 +4,7 @@ import json import os +import subprocess import uuid from pathlib import Path @@ -23,6 +24,7 @@ _extract_first_prompt_from_head, _extract_json_string_field, _extract_last_json_string_field, + _get_worktree_paths, _parse_session_info_from_lite, _read_session_lite, _sanitize_path, @@ -161,6 +163,49 @@ def test_simple_hash_zero(self): # Empty string should produce "0" assert _simple_hash("") == "0" + def test_get_worktree_paths_pins_utf8_decoding( + self, monkeypatch: pytest.MonkeyPatch + ): + """git emits worktree paths as raw UTF-8, so the decode must be pinned. + + Without an explicit encoding, subprocess decodes the child's output + with the locale default, which is cp1252 on a stock Windows install. + That misreads a non-ASCII worktree path in one of two ways: + + * If the path's UTF-8 contains 0x81, 0x8D, 0x8F, 0x90 or 0x9D -- the + five bytes cp1252 leaves undefined, which covers all hiragana + (U+3040-307F encode as E3 81 xx), U+200D ZWJ in emoji sequences, + and Cyrillic such as U+0441 -- the decode raises UnicodeDecodeError + inside subprocess's reader thread. That never reaches the caller, + which instead sees returncode 0 with stdout None, and the + `not result.stdout` guard turns it into an empty list. A single + such worktree hides every worktree in the repo, ASCII ones included. + * Otherwise the bytes decode to mojibake, so the returned path is + well-formed but does not exist on disk. + + Pinning UTF-8 fixes both. It is also the codec os.fsdecode uses here, + since sys.getfilesystemencoding() is utf-8 on Windows (PEP 529) and + on POSIX. + """ + captured: dict[str, object] = {} + listing = "worktree /repo\nworktree /repo/сессия\n" + + def fake_run(*args, **kwargs): + captured.update(kwargs) + return subprocess.CompletedProcess( + args=["git", "worktree", "list", "--porcelain"], + returncode=0, + stdout=listing, + stderr="", + ) + + monkeypatch.setattr(subprocess, "run", fake_run) + + paths = _get_worktree_paths("/repo") + + assert captured.get("encoding") == "utf-8" + assert paths == ["/repo", "/repo/сессия"] + def test_extract_json_string_field_simple(self): text = '{"foo":"bar","baz":"qux"}' assert _extract_json_string_field(text, "foo") == "bar"