{"repo":"NousResearch/hermes-agent","public_id":"39834","display_label":"Windows MSYS path mangling bugs and fixes","summary":"These issues and pull requests all concern incorrect handling of MSYS/Git Bash-style paths (e.g. /c/Users/...) on Windows, where paths are misresolved or doubled, causing writes to wrong locations, broken tooling, and related failures. Multiple fix attempts and bug reports address the same recurring conversion problem across file tools, shell lint, logging, and snapshots.","subsystem":"agent","counts":{"members":13,"open_issues":3,"open_issues_distinct":3,"open_prs":3,"closed_issues":2,"merged_prs":1,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":61915},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-07-10T07:03:43+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.13384615384615384},"fix_edges":[{"issue_number":44726,"pr_number":61915,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":46876,"pr_number":61915,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":39834,"title":"[Windows] MSYS2 path mangling creates phantom C:\\c directory — breaks logging, cron, and sessions","url":null,"state":"open"},{"kind":"issue","number":44726,"title":"[Windows] write_file/patch silently write to wrong path when model uses MSYS-style paths (/d/Project → D:\\d\\Project)","url":null,"state":"closed"},{"kind":"issue","number":46876,"title":"[Bug]: Windows MSYS path resolution breaks file writes after Hermes upgrade","url":null,"state":"closed"},{"kind":"issue","number":62998,"title":"Windows: MSYS path /c/Users/... incorrectly converted to C:\\c\\Users\\... in tool output (recurring, 3 occurrences)","url":null,"state":"open"},{"kind":"issue","number":66494,"title":"Windows/MSYS: automatic file lint fails — path built as E:\\ + /e/... (module not found)","url":null,"state":"open"},{"kind":"pr","number":39910,"title":"fix(windows): unmangle MSYS Hermes paths","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"5a19ce64a","number":0}],"category":"wontfix_bydesign","implication":"wontfix"},"merged":false},{"kind":"pr","number":44735,"title":"fix(file-tools): normalize MSYS/Git Bash paths on Windows","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3f8b22004958506e9d476989365b3ee2ced4ecff","number":0},{"kind":"pr","value":"#61915","number":61915}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":44765,"title":"fix(file-tools): normalize MSYS paths in _resolve_path_for_task on Windows (#44726)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#44735","number":44735}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":46887,"title":"fix(file-tools): normalize MSYS paths to prevent Windows ghost tree writes","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#44735","number":44735},{"kind":"pr","value":"#46876","number":46876},{"kind":"pr","value":"#44735","number":44735},{"kind":"pr","value":"#44735","number":44735},{"kind":"pr","value":"#44765","number":44765},{"kind":"pr","value":"#46876","number":46876}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":46995,"title":"fix(file-tools): normalize Windows MSYS task paths","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"3f8b22004958506e9d476989365b3ee2ced4ecff","number":0},{"kind":"pr","value":"#61915","number":61915},{"kind":"pr","value":"#46995","number":46995}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":61915,"title":"fix(tools): resolve MSYS paths in file tools on Windows (supersedes #50488)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_file_tools.py","tests/tools/test_local_env_windows_msys.py","tools/environments/local.py","tools/file_tools.py"],"fix_hunks":[{"file":"tests/tools/test_file_tools.py","hunks":["@@ -427,6 +427,69 @@ def test_search_exception_returns_error(self, mock_get):\n         assert \"error\" in result\n \n \n+# ---------------------------------------------------------------------------\n+# Windows MSYS path resolution (salvage of #50488 / #46995)\n+# ---------------------------------------------------------------------------\n+\n+class TestWindowsMsysPathResolution:\n+    \"\"\"File tools must translate Git Bash drive paths before Path resolution.\"\"\"\n+\n+    def test_absolute_msys_path_normalized_before_windows_resolve(self, monkeypatch):\n+        import tools.environments.local as local_mod\n+        import tools.file_tools as file_tools\n+\n+        monkeypatch.setattr(file_tools.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        monkeypatch.setattr(file_tools, \"_uses_container_paths\", lambda task_id=\"default\": False)\n+\n+        resolved = file_tools._resolve_path_for_task(\"/c/Users/Mark/project/app.py\")\n+        assert str(resolved) == r\"C:\\Users\\Mark\\project\\app.py\"\n+\n+    def test_cygdrive_path_normalized(self, monkeypatch):\n+        import tools.environments.local as local_mod\n+        import tools.file_tools as file_tools\n+\n+        monkeypatch.setattr(file_tools.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        monkeypatch.setattr(file_tools, \"_uses_container_paths\", lambda task_id=\"default\": False)\n+\n+        resolved = file_tools._resolve_path_for_task(\"/cygdrive/d/code/main.py\")\n+        assert str(resolved) == r\"D:\\code\\main.py\"\n+\n+    def test_relative_path_uses_normalized_msys_cwd(self, monkeypatch):\n+        import tools.environments.local as local_mod\n+        import tools.file_tools as file_tools\n+\n+        monkeypatch.setattr(file_tools.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        monkeypatch.setattr(file_tools, \"_uses_container_paths\", lambda task_id=\"default\": False)\n+        monkeypatch.setattr(\n+            file_tools,\n+            \"_authoritative_workspace_root\",\n+            lambda task_id=\"default\": \"/c/Users/Mark/project\",\n+        )\n+\n+        resolved = file_tools._resolve_path_for_task(\"src/app.py\", task_id=\"msys\")\n+        assert str(resolved) == r\"C:\\Users\\Mark\\project\\src\\app.py\"\n+\n+    def test_container_paths_skip_msys_translation(self, monkeypatch):\n+        \"\"\"WSL/docker Linux paths must not be rewritten as Windows drives.\"\"\"\n+        import tools.environments.local as local_mod\n+        import tools.file_tools as file_tools\n+\n+        monkeypatch.setattr(file_tools.sys, \"platform\", \"win32\")\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        monkeypatch.setattr(file_tools, \"_uses_container_paths\", lambda task_id=\"default\": True)\n+        monkeypatch.setattr(\n+            file_tools,\n+            \"_authoritative_workspace_root\",\n+            lambda task_id=\"default\": \"/home/don/project\",\n+        )\n+\n+        resolved = file_tools._resolve_path_for_task(\"/home/don/.env\")\n+        assert str(resolved) == \"/home/don/.env\"\n+\n+\n # ---------------------------------------------------------------------------\n # Tool result hint tests (#722)\n # ---------------------------------------------------------------------------"]},{"file":"tests/tools/test_local_env_windows_msys.py","hunks":["@@ -68,6 +68,15 @@ def test_does_not_translate_multi_char_first_segment(self, monkeypatch):\n         monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n         assert _msys_to_windows_path(\"/tmp/foo\") == \"/tmp/foo\"\n         assert _msys_to_windows_path(\"/home/x\") == \"/home/x\"\n+        # /mnt/<name>/... only translates when <name> is a single drive letter.\n+        assert _msys_to_windows_path(\"/mnt/home/x\") == \"/mnt/home/x\"\n+\n+    def test_translates_cygdrive_and_wsl_mnt_forms(self, monkeypatch):\n+        monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)\n+        assert _msys_to_windows_path(\"/cygdrive/c/Users/NVIDIA\") == r\"C:\\Users\\NVIDIA\"\n+        assert _msys_to_windows_path(\"/mnt/d/Projects/foo\") == r\"D:\\Projects\\foo\"\n+        assert _msys_to_windows_path(\"/cygdrive/c\") == \"C:\\\\\"\n+        assert _msys_to_windows_path(\"/mnt/c/\") == \"C:\\\\\"\n \n     def test_empty_string(self, monkeypatch):\n         monkeypatch.setattr(local_mod, \"_IS_WINDOWS\", True)"]},{"file":"tools/environments/local.py","hunks":["@@ -25,16 +25,24 @@ def _msys_to_windows_path(cwd: str) -> str:\n     native Windows form (``C:\\\\Users\\\\x``) so ``os.path.isdir`` and\n     ``subprocess.Popen(..., cwd=...)`` can find it.\n \n+    Also accepts the Cygwin (``/cygdrive/c/...``) and WSL-mount\n+    (``/mnt/c/...``) spellings of a drive root. Multi-segment POSIX paths\n+    like ``/home/x`` or ``/tmp/foo`` are left untouched.\n+\n     No-ops on non-Windows hosts or for paths that aren't in MSYS form.\n     Returns the input unchanged when no translation applies. This is\n     idempotent — calling it on an already-Windows path returns it as-is.\n     \"\"\"\n     if not _IS_WINDOWS or not cwd:\n         return cwd\n-    # Match leading \"/<single letter>/\" or exactly \"/<letter>\" (bare drive root).\n-    m = re.match(r'^/([a-zA-Z])(/.*)?$', cwd)\n+    # Match leading \"/<single letter>/\" or exactly \"/<letter>\" (bare drive root),\n+    # plus /cygdrive/<letter>/... and /mnt/<letter>/... variants.\n+    m = re.match(r'^/(?:(?:cygdrive|mnt)/)?([a-zA-Z])(/.*)?$', cwd)\n     if not m:\n         return cwd\n+    # Reject /cygdrive or /mnt with no drive letter — the optional group above\n+    # already requires the letter. Multi-char first segments (/home, /tmp)\n+    # fail the single-letter capture and fall through as no-ops.\n     drive = m.group(1).upper()\n     tail = (m.group(2) or \"\").replace('/', '\\\\')\n     return f\"{drive}:{tail or chr(92)}\"  # chr(92) = backslash, avoid raw-string escape"]},{"file":"tools/file_tools.py","hunks":["@@ -6,6 +6,7 @@\n import logging\n import os\n import posixpath\n+import sys\n import threading\n from pathlib import Path, PurePosixPath\n ","@@ -406,6 +407,17 @@ def _resolve_base_dir(\n         if not posixpath.isabs(base_text):\n             base_text = posixpath.join(os.getcwd(), base_text)\n         return _normalize_without_host_deref(base_text)\n+    # Git Bash ``pwd -P`` reports ``/c/Users/...``; translate before Path so\n+    # relative file-tool paths don't anchor under a nonexistent ``\\\\c\\\\Users``.\n+    from tools.environments.local import _msys_to_windows_path\n+\n+    base_text = _msys_to_windows_path(base_text)\n+    if sys.platform == \"win32\":\n+        import ntpath\n+\n+        if not ntpath.isabs(base_text):\n+            base_text = ntpath.join(os.getcwd(), base_text)\n+        return Path(ntpath.normpath(base_text))\n     base = Path(base_text)\n     if not base.is_absolute():\n         # Last-resort anchoring: a live cwd should already be absolute, but if a","@@ -420,14 +432,31 @@ def _resolve_path_for_task(filepath: str, task_id: str = \"default\") -> Path | Pu\n \n     See :func:`_resolve_base_dir` for how the base is chosen. Absolute input\n     paths are returned resolved-but-unanchored.\n+\n+    On native Windows, Git Bash / MSYS drive paths (``/c/Users/...``) are\n+    translated to ``C:\\\\Users\\\\...`` before resolution so file tools don't\n+    treat them as relative ``\\\\c\\\\Users\\\\...`` under the process cwd.\n     \"\"\"\n     container_paths = _uses_container_paths(task_id)\n-    expanded = _expand_tilde(filepath)\n     if container_paths:\n+        expanded = _expand_tilde(filepath)\n         if posixpath.isabs(expanded):\n             return _normalize_without_host_deref(expanded)\n         resolved = _resolve_base_dir(task_id, container_paths=True) / expanded\n         return _normalize_without_host_deref(resolved)\n+\n+    # Host paths only — never rewrite Linux paths inside a container/WSL env.\n+    from tools.environments.local import _msys_to_windows_path\n+\n+    expanded = _expand_tilde(_msys_to_windows_path(filepath))\n+    if sys.platform == \"win32\":\n+        import ntpath\n+\n+        if ntpath.isabs(expanded):\n+            return Path(ntpath.normpath(expanded))\n+        joined = ntpath.join(str(_resolve_base_dir(task_id, container_paths=False)), expanded)\n+        return Path(ntpath.normpath(joined))\n+\n     p = Path(expanded)\n     if p.is_absolute():\n         return p.resolve()\n"]}]},{"kind":"pr","number":63012,"title":"fix(prompt): clarify Windows MSYS path mapping","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":66524,"title":"fix(tools): translate MSYS paths to Windows-native before shell lint (#66494)","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}