# NousResearch/hermes-agent · Windows MSYS path mangling bugs and fixes

## Open now

- 3 issues, 3 PRs open on GitHub.
- #39834: \[Windows\] MSYS2 path mangling creates phantom C:\\c directory — breaks logging, cron, and sessions
- #62998: Windows: MSYS path /c/Users/... incorrectly converted to C:\\c\\Users\\... in tool output (recurring, 3 occurrences)
- #66494: Windows/MSYS: automatic file lint fails — path built as E:\\ + /e/... (module not found)
- PR #39910: fix(windows): unmangle MSYS Hermes paths
- PR #63012: fix(prompt): clarify Windows MSYS path mapping
- PR #66524: fix(tools): translate MSYS paths to Windows-native before shell lint (#66494)

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.

## TL;DR

- 13 member(s): 3 open issue(s), 2 closed issue(s), 3 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #61915. Verify it addresses your case.
- **Recommended:** read PR #61915 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

- #39834: \[Windows\] MSYS2 path mangling creates phantom C:\\c directory — breaks logging, cron, and sessions
- #62998: Windows: MSYS path /c/Users/... incorrectly converted to C:\\c\\Users\\... in tool output (recurring, 3 occurrences)
- #66494: Windows/MSYS: automatic file lint fails — path built as E:\\ + /e/... (module not found)

## Closed / duplicate issues

- #44726: \[Windows\] write_file/patch silently write to wrong path when model uses MSYS-style paths (/d/Project → D:\\d\\Project)
- #46876: \[Bug\]: Windows MSYS path resolution breaks file writes after Hermes upgrade

## Prior & competing PRs

- **#61915: fix(tools): resolve MSYS paths in file tools on Windows (supersedes #50488)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_file_tools.py`, `tests/tools/test_local_env_windows_msys.py`, `tools/environments/local.py`, `tools/file_tools.py`
  - fix hunks:
  - `tests/tools/test_file_tools.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -6,6 +6,7 @@
     import logging
     import os
     import posixpath
    +import sys
     import threading
     from pathlib import Path, PurePosixPath
     
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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()
    ```
- **#39910: fix(windows): unmangle MSYS Hermes paths**. open
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `5a19ce64a`
  - files: `cron/executions.py`, `cron/jobs.py`, `cron/suggestions.py`, `gateway/status.py`, `hermes_constants.py`, `hermes_logging.py`, `tests/cron/test_execution_ledger.py`, `tests/cron/test_jobs.py`, `tests/cron/test_suggestions.py`, `tests/gateway/test_status.py`, `tests/test_hermes_constants.py`, `tests/test_hermes_logging.py`
- **#44735: fix(file-tools): normalize MSYS/Git Bash paths on Windows**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `3f8b22004958506e9d476989365b3ee2ced4ecff`, `#61915`
- **#44765: fix(file-tools): normalize MSYS paths in _resolve_path_for_task on Windows (#44726)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#44735`
- **#46887: fix(file-tools): normalize MSYS paths to prevent Windows ghost tree writes**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#44735`, `#46876`, `#44735`, `#44735`, `#44765`, `#46876`
- **#46995: fix(file-tools): normalize Windows MSYS task paths**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `3f8b22004958506e9d476989365b3ee2ced4ecff`, `#61915`, `#46995`
- **#63012: fix(prompt): clarify Windows MSYS path mapping**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `agent/prompt_builder.py`, `skills/autonomous-ai-agents/hermes-agent/SKILL.md`, `tests/agent/test_prompt_builder.py`
- **#66524: fix(tools): translate MSYS paths to Windows-native before shell lint (#66494)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `contributors/emails/[redacted:email]`, `tests/tools/test_file_operations_edge_cases.py`, `tools/file_operations.py`

## Fix status

- Merged to main in PR #61915 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-10`.
- Subsystem: `agent`
