{"repo":"NousResearch/hermes-agent","public_id":"35459","display_label":"Windows/WSL path format mismatch in terminal CWD handling","summary":"These issues and pull requests concern a problem where Windows-native path formats leak into terminal sessions running under WSL or Git Bash, causing 'cd' failures and broken subsequent commands. The work involves detecting and converting Windows paths to MSYS/Unix-compatible formats for terminal tool operations.","subsystem":"environments","counts":{"members":10,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":4,"merged_prs":1,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56384},"resolution":{"state":"partially_resolved","by":"pr-56384"},"freshness":{"state":"verified","last_verified_at":"2026-07-01T12:35:35+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.18755555555555553},"fix_edges":[{"issue_number":35459,"pr_number":56384,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":47837,"pr_number":56384,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":50594,"pr_number":56384,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":35459,"title":"Windows terminal: cd fails with 'No such file or directory' for Windows native paths","url":null,"state":"closed"},{"kind":"issue","number":40137,"title":"Terminal wrapper injects Windows paths when running in WSL","url":null,"state":"closed"},{"kind":"issue","number":50594,"title":"Windows: session cwd in native format breaks terminal tool","url":null,"state":"closed"},{"kind":"issue","number":50820,"title":"[BUG] terminal tool CWD wedge: Windows session CWD leaks into WSL bash as invalid path","url":null,"state":"closed"},{"kind":"pr","number":13397,"title":"fix: Windows stale-cwd causes exit 126 on all subsequent commands","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#50594","number":50594},{"kind":"pr","value":"#56384","number":56384},{"kind":"pr","value":"#17558","number":17558}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":35653,"title":"fix(terminal): normalize Windows cwd for Git Bash cd","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9ed7252a9","number":0},{"kind":"pr","value":"#56384","number":56384}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":40247,"title":"fix(wsl): translate Windows cwd values for terminal tools","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3c7b9f2e9d00025216e663e024b1ee6f415d959c","number":0},{"kind":"pr","value":"#40137","number":40137}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":49833,"title":"fix(terminal): resolve Windows path format mismatch in LocalEnvironment","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"ad5f3341d","number":0},{"kind":"commit","value":"5d613a563","number":0},{"kind":"pr","value":"#56384","number":56384},{"kind":"pr","value":"#47837","number":47837}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":50596,"title":"fix(tools): convert Windows native CWD to MSYS format for bash cd","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#56384","number":56384}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":56384,"title":"fix(terminal): force Git for Windows bash + convert native CWD for bash cd","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_local_env_windows_msys.py","tools/environments/base.py","tools/environments/local.py"],"fix_hunks":[{"file":"tests/tools/test_local_env_windows_msys.py","hunks":["@@ -26,6 +26,7 @@\n     LocalEnvironment,\n     _msys_to_windows_path,\n     _resolve_safe_cwd,\n+    _windows_to_msys_path,\n )\n \n ","@@ -70,6 +71,35 @@ def test_empty_string(self, monkeypatch):\n         assert _msys_to_windows_path(\"\") == \"\"\n \n \n+# ---------------------------------------------------------------------------\n+# _windows_to_msys_path — reverse translation for bash builtin cd\n+# ---------------------------------------------------------------------------\n+\n+class TestWindowsToMsysPath:\n+    def test_noop_on_non_windows(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", False)\n+        assert _windows_to_msys_path(r\"C:\\Users\\NVIDIA\") == r\"C:\\Users\\NVIDIA\"\n+\n+    def test_translates_backslash_path(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        assert _windows_to_msys_path(r\"C:\\Users\\NVIDIA\") == \"/c/Users/NVIDIA\"\n+        assert _windows_to_msys_path(r\"D:\\Projects\\foo bar\") == \"/d/Projects/foo bar\"\n+\n+    def test_translates_forward_slash_native_path(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        assert _windows_to_msys_path(\"C:/Users/NVIDIA\") == \"/c/Users/NVIDIA\"\n+\n+    def test_translates_drive_root(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        assert _windows_to_msys_path(r\"C:\\\\\") == \"/c/\"\n+        assert _windows_to_msys_path(\"D:/\") == \"/d/\"\n+\n+    def test_does_not_translate_non_drive_path(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        assert _windows_to_msys_path(\"/tmp/foo\") == \"/tmp/foo\"\n+        assert _windows_to_msys_path(r\"\\\\server\\share\") == r\"\\\\server\\share\"\n+\n+\n # ---------------------------------------------------------------------------\n # _resolve_safe_cwd — Windows fast path\n # ---------------------------------------------------------------------------","@@ -196,3 +226,44 @@ def test_valid_msys_marker_normalized_to_native(self, monkeypatch, tmp_path):\n             env._extract_cwd_from_output(result)\n \n         assert env.cwd == str(new_dir)\n+\n+\n+# ---------------------------------------------------------------------------\n+# Command wrapping — native Windows cwd must be Git Bash-friendly for cd\n+# ---------------------------------------------------------------------------\n+\n+class TestWrapCommandWindowsNativeCwd:\n+    def test_wrap_command_converts_native_cwd_for_builtin_cd(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+\n+        with patch.object(\n+            LocalEnvironment, \"init_session\", autospec=True, return_value=None\n+        ):\n+            env = LocalEnvironment(cwd=r\"C:\\Users\\liush\", timeout=10)\n+\n+        env._snapshot_ready = True\n+        wrapped = env._wrap_command(\"pwd\", r\"C:\\Users\\liush\")\n+\n+        assert \"builtin cd -- /c/Users/liush || exit 126\" in wrapped\n+        assert r\"builtin cd -- C:\\Users\\liush || exit 126\" not in wrapped\n+\n+    def test_init_session_bootstrap_converts_native_cwd_for_cd(self, monkeypatch):\n+        \"\"\"The snapshot bootstrap ``cd`` must also use the Git-Bash path form,\n+        not just ``_wrap_command`` — otherwise ``pwd -P`` captures the login\n+        shell's directory instead of ``terminal.cwd`` on Windows.\"\"\"\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+\n+        captured = {}\n+\n+        def fake_run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None):\n+            captured[\"script\"] = cmd_string\n+            raise RuntimeError(\"stop after capturing bootstrap\")\n+\n+        monkeypatch.setattr(LocalEnvironment, \"_run_bash\", fake_run_bash)\n+\n+        # init_session swallows the exception and falls back; we only need the\n+        # captured bootstrap script to assert the cd target was converted.\n+        LocalEnvironment(cwd=r\"C:\\Users\\liush\", timeout=10)\n+\n+        assert \"builtin cd -- /c/Users/liush 2>/dev/null || true\" in captured[\"script\"]\n+        assert r\"C:\\Users\\liush\" not in captured[\"script\"]"]},{"file":"tools/environments/base.py","hunks":["@@ -361,7 +361,12 @@ def init_session(self):\n         # Restore configured cwd after login shell profile scripts, which may\n         # change the working directory (e.g. bashrc `cd ~`).  Without this,\n         # pwd -P captures the profile's directory, not terminal.cwd.\n-        _quoted_cwd = shlex.quote(self.cwd)\n+        # Route through ``_quote_cwd_for_cd`` (not a bare ``shlex.quote``) so\n+        # the Windows subclass override converts a native ``C:\\Users\\x`` cwd to\n+        # the Git-Bash ``/c/Users/x`` form the bootstrap ``cd`` can resolve.\n+        # Without this the snapshot bootstrap ``cd`` below fails on Windows and\n+        # ``pwd -P`` captures the login shell's directory, not ``terminal.cwd``.\n+        _quoted_cwd = self._quote_cwd_for_cd(self.cwd)\n         # Quote the snapshot / cwd-file paths so Git Bash on Windows handles\n         # ``C:/Users/...``-shaped paths without glob-splitting the colon or\n         # tripping on drive letters.  On POSIX this is a no-op (no colons /","@@ -413,7 +418,7 @@ def init_session(self):\n             # Publish atomically only if assembly succeeded; otherwise drop the\n             # partial temp rather than leave it to be sourced or orphaned.\n             f\"mv -f {_snap_tmp} {_quoted_snap} || rm -f {_snap_tmp}\\n\"\n-            f\"builtin cd {_quoted_cwd} 2>/dev/null || true\\n\"\n+            f\"builtin cd -- {_quoted_cwd} 2>/dev/null || true\\n\"\n             f\"pwd -P > {_quoted_cwd_file} 2>/dev/null || true\\n\"\n             f\"printf '\\\\n{self._cwd_marker}%s{self._cwd_marker}\\\\n' \\\"$(pwd -P)\\\"\\n\"\n         )"]},{"file":"tools/environments/local.py","hunks":["@@ -40,6 +40,24 @@ def _msys_to_windows_path(cwd: str) -> str:\n     return f\"{drive}:{tail or chr(92)}\"  # chr(92) = backslash, avoid raw-string escape\n \n \n+def _windows_to_msys_path(cwd: str) -> str:\n+    \"\"\"Translate a native Windows path (``C:\\\\Users\\\\x``) to Git Bash /\n+    MSYS form (``/c/Users/x``) so ``builtin cd`` resolves it reliably.\n+\n+    No-ops on non-Windows hosts or for paths that aren't drive-qualified\n+    native Windows paths. Returns the input unchanged when no translation\n+    applies.\n+    \"\"\"\n+    if not _IS_WINDOWS or not cwd:\n+        return cwd\n+    m = re.match(r'^([a-zA-Z]):[\\\\/]*(.*)$', cwd)\n+    if not m:\n+        return cwd\n+    drive = m.group(1).lower()\n+    tail = (m.group(2) or \"\").replace('\\\\', '/').lstrip('/')\n+    return f\"/{drive}/{tail}\" if tail else f\"/{drive}/\"\n+\n+\n def _resolve_safe_cwd(cwd: str) -> str:\n     \"\"\"Return ``cwd`` if it exists as a directory, else the nearest existing\n     ancestor.  Falls back to ``tempfile.gettempdir()`` only if walking up the","@@ -509,10 +527,10 @@ def _find_bash() -> str:\n             if os.path.isfile(candidate):\n                 return candidate\n \n-    found = shutil.which(\"bash\")\n-    if found:\n-        return found\n-\n+    # Check known Git for Windows install locations before PATH lookup.\n+    # On machines with both WSL and Git for Windows, shutil.which(\"bash\")\n+    # may return WSL's bash (which doesn't understand Windows paths and\n+    # will fail silently).  Explicit Git-for-Windows paths avoid that.\n     for candidate in (\n         os.path.join(os.environ.get(\"ProgramFiles\", r\"C:\\Program Files\"), \"Git\", \"bin\", \"bash.exe\"),\n         os.path.join(os.environ.get(\"ProgramFiles(x86)\", r\"C:\\Program Files (x86)\"), \"Git\", \"bin\", \"bash.exe\"),","@@ -521,6 +539,10 @@ def _find_bash() -> str:\n         if candidate and os.path.isfile(candidate):\n             return candidate\n \n+    found = shutil.which(\"bash\")\n+    if found:\n+        return found\n+\n     raise RuntimeError(\n         \"Git Bash not found. Hermes Agent requires Git for Windows on Windows.\\n\"\n         \"Install it from: https://git-scm.com/download/win\\n\"","@@ -917,6 +939,11 @@ def get_temp_dir(self) -> str:\n \n         return \"/tmp\"\n \n+    @staticmethod\n+    def _quote_cwd_for_cd(cwd: str) -> str:\n+        \"\"\"Use native paths for Python, but Git Bash-friendly paths for cd.\"\"\"\n+        return BaseEnvironment._quote_cwd_for_cd(_windows_to_msys_path(cwd))\n+\n     def _run_bash(self, cmd_string: str, *, login: bool = False,\n                   timeout: int = 120,\n                   stdin_data: str | None = None) -> subprocess.Popen:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}