Windows MSYS path mangling bugs and fixes
NousResearch/hermes-agent · cluster 39834 · agent
Data as of
6 open items. Fix merged in #61915. Check it covers your case.
The short answer
- Known?
- Yes. 13 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #61915. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- 3 issues and 3 PRs.
Next step: read #61915, then check whether your report is one of the 3 still open.
Read #61915Pick up a Lab taskThese 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.
Open issues3
- Issue#39834[Windows] MSYS2 path mangling creates phantom C:\c directory — breaks logging, cron, and sessions
- Issue#62998Windows: MSYS path /c/Users/... incorrectly converted to C:\c\Users\... in tool output (recurring, 3 occurrences)
- Issue#66494Windows/MSYS: automatic file lint fails — path built as E:\ + /e/... (module not found)
Open PRs3
Fixes on record
-
#61915 fix(tools): resolve MSYS paths in file tools on Windows (supersedes #50488)
files:
tests/tools/test_file_tools.py,tests/tools/test_local_env_windows_msys.py,tools/environments/local.py,tools/file_tools.pyShow the diff
-
tests/tools/test_file_tools.py@@ -427,6 +427,69 @@ def test_search_exception_returns_error(self, mock_get): assert "error" in result +# --------------------------------------------------------------------------- +# Windows MSYS path resolution (salvage of #50488 / #46995) +# --------------------------------------------------------------------------- + +class TestWindowsMsysPathResolution: + """File tools must translate Git Bash drive paths before Path resolution.""" + + def test_absolute_msys_path_normalized_before_windows_resolve(self, monkeypatch): + import tools.environments.local as local_mod + import tools.file_tools as file_tools + + monkeypatch.setattr(file_tools.sys, "platform", "win32") + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": False) + + resolved = file_tools._resolve_path_for_task("/c/Users/Mark/project/app.py") + assert str(resolved) == r"C:\Users\Mark\project\app.py" + + def test_cygdrive_path_normalized(self, monkeypatch): + import tools.environments.local as local_mod + import tools.file_tools as file_tools + + monkeypatch.setattr(file_tools.sys, "platform", "win32") + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": False) + + resolved = file_tools._resolve_path_for_task("/cygdrive/d/code/main.py") + assert str(resolved) == r"D:\code\main.py" + + def test_relative_path_uses_normalized_msys_cwd(self, monkeypatch): + import tools.environments.local as local_mod + import tools.file_tools as file_tools + + monkeypatch.setattr(file_tools.sys, "platform", "win32") + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": False) + monkeypatch.setattr( + file_tools, + "_authoritative_workspace_root", + lambda task_id="default": "/c/Users/Mark/project", + ) + + resolved = file_tools._resolve_path_for_task("src/app.py", task_id="msys") + assert str(resolved) == r"C:\Users\Mark\project\src\app.py" + + def test_container_paths_skip_msys_translation(self, monkeypatch): + """WSL/docker Linux paths must not be rewritten as Windows drives.""" + import tools.environments.local as local_mod + import tools.file_tools as file_tools + + monkeypatch.setattr(file_tools.sys, "platform", "win32") + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + monkeypatch.setattr(file_tools, "_uses_container_paths", lambda task_id="default": True) + monkeypatch.setattr( + file_tools, + "_authoritative_workspace_root", + lambda task_id="default": "/home/don/project", + ) + + resolved = file_tools._resolve_path_for_task("/home/don/.env") + assert str(resolved) == "/home/don/.env" + + # --------------------------------------------------------------------------- # Tool result hint tests (#722) # --------------------------------------------------------------------------- -
tests/tools/test_local_env_windows_msys.py@@ -68,6 +68,15 @@ def test_does_not_translate_multi_char_first_segment(self, monkeypatch): monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) assert _msys_to_windows_path("/tmp/foo") == "/tmp/foo" assert _msys_to_windows_path("/home/x") == "/home/x" + # /mnt/<name>/... only translates when <name> is a single drive letter. + assert _msys_to_windows_path("/mnt/home/x") == "/mnt/home/x" + + def test_translates_cygdrive_and_wsl_mnt_forms(self, monkeypatch): + monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) + assert _msys_to_windows_path("/cygdrive/c/Users/NVIDIA") == r"C:\Users\NVIDIA" + assert _msys_to_windows_path("/mnt/d/Projects/foo") == r"D:\Projects\foo" + assert _msys_to_windows_path("/cygdrive/c") == "C:\\" + assert _msys_to_windows_path("/mnt/c/") == "C:\\" def test_empty_string(self, monkeypatch): monkeypatch.setattr(local_mod, "_IS_WINDOWS", True) -
tools/environments/local.py@@ -25,16 +25,24 @@ def _msys_to_windows_path(cwd: str) -> str: native Windows form (``C:\\Users\\x``) so ``os.path.isdir`` and ``subprocess.Popen(..., cwd=...)`` can find it. + Also accepts the Cygwin (``/cygdrive/c/...``) and WSL-mount + (``/mnt/c/...``) spellings of a drive root. Multi-segment POSIX paths + like ``/home/x`` or ``/tmp/foo`` are left untouched. + No-ops on non-Windows hosts or for paths that aren't in MSYS form. Returns the input unchanged when no translation applies. This is idempotent — calling it on an already-Windows path returns it as-is. """ if not _IS_WINDOWS or not cwd: return cwd - # Match leading "/<single letter>/" or exactly "/<letter>" (bare drive root). - m = re.match(r'^/([a-zA-Z])(/.*)?$', cwd) + # Match leading "/<single letter>/" or exactly "/<letter>" (bare drive root), + # plus /cygdrive/<letter>/... and /mnt/<letter>/... variants. + m = re.match(r'^/(?:(?:cygdrive|mnt)/)?([a-zA-Z])(/.*)?$', cwd) if not m: return cwd + # Reject /cygdrive or /mnt with no drive letter — the optional group above + # already requires the letter. Multi-char first segments (/home, /tmp) + # fail the single-letter capture and fall through as no-ops. drive = m.group(1).upper() tail = (m.group(2) or "").replace('/', '\\') return f"{drive}:{tail or chr(92)}" # chr(92) = backslash, avoid raw-string escape -
tools/file_tools.py@@ -6,6 +6,7 @@ import logging import os import posixpath +import sys import threading from pathlib import Path, PurePosixPath @@ -406,6 +407,17 @@ def _resolve_base_dir( if not posixpath.isabs(base_text): base_text = posixpath.join(os.getcwd(), base_text) return _normalize_without_host_deref(base_text) + # Git Bash ``pwd -P`` reports ``/c/Users/...``; translate before Path so + # relative file-tool paths don't anchor under a nonexistent ``\\c\\Users``. + from tools.environments.local import _msys_to_windows_path + + base_text = _msys_to_windows_path(base_text) + if sys.platform == "win32": + import ntpath + + if not ntpath.isabs(base_text): + base_text = ntpath.join(os.getcwd(), base_text) + return Path(ntpath.normpath(base_text)) base = Path(base_text) if not base.is_absolute(): # 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 See :func:`_resolve_base_dir` for how the base is chosen. Absolute input paths are returned resolved-but-unanchored. + + On native Windows, Git Bash / MSYS drive paths (``/c/Users/...``) are + translated to ``C:\\Users\\...`` before resolution so file tools don't + treat them as relative ``\\c\\Users\\...`` under the process cwd. """ container_paths = _uses_container_paths(task_id) - expanded = _expand_tilde(filepath) if container_paths: + expanded = _expand_tilde(filepath) if posixpath.isabs(expanded): return _normalize_without_host_deref(expanded) resolved = _resolve_base_dir(task_id, container_paths=True) / expanded return _normalize_without_host_deref(resolved) + + # Host paths only — never rewrite Linux paths inside a container/WSL env. + from tools.environments.local import _msys_to_windows_path + + expanded = _expand_tilde(_msys_to_windows_path(filepath)) + if sys.platform == "win32": + import ntpath + + if ntpath.isabs(expanded): + return Path(ntpath.normpath(expanded)) + joined = ntpath.join(str(_resolve_base_dir(task_id, container_paths=False)), expanded) + return Path(ntpath.normpath(joined)) + p = Path(expanded) if p.is_absolute(): return p.resolve()
-
4 PRs closed without merging
From the close comments: duplicate 2 · done elsewhere 1 · merged via another change 1
-
#44735 fix(file-tools): normalize MSYS/Git Bash paths on Windows · done elsewhere →
3f8b220,#61915 -
#44765 fix(file-tools): normalize MSYS paths in _resolve_path_for_task on Windows (#44726) · duplicate →
#44735 -
#46887 fix(file-tools): normalize MSYS paths to prevent Windows ghost tree writes · duplicate →
#44735,#46876,#44765 -
#46995 fix(file-tools): normalize Windows MSYS task paths · merged via another change →
3f8b220,#61915,#46995
Closed issues · 2
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.13: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means 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 LabAt a glance
- Subsystem
- agent
- Merged PRs
- #61915
- Closed issues
- 2
- Attempts
- 4 PRs closed without merging
- Fix link checked
- 10 Jul 2026