# NousResearch/hermes-agent · Gateway MEDIA tag Windows file path parsing fixes

## Open now

- Nothing open on GitHub.

Multiple bug reports and fixes addressing failures in gateway regex logic to correctly parse Windows-style absolute paths, including drive letters and backslashes, in MEDIA tags.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #35388. Verify it addresses your case.
- **Recommended:** read PR #35388 (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: `resolved` · deciding PR(s): `pr-35388`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: 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.

## Closed / duplicate issues

- #35270: \[Bug\]: Gateway extract_media and extract_local_files fail to parse Windows file paths

## Prior & competing PRs

- **#35388: fix(gateway): support Windows absolute paths in MEDIA tag regexes**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `gateway/platforms/base.py`, `gateway/run.py`, `tests/gateway/test_extract_local_files.py`, `tests/gateway/test_platform_base.py`, `tests/gateway/test_run_tool_media_re.py`
  - fix hunks:
  - `gateway/platforms/base.py`

    ```diff
    @@ -1191,10 +1191,12 @@ def _log_safe_path(path: str) -> str:
     # bare-path detector (extract_local_files) downstream rather than silently
     # deleted. Shared by the non-streaming dispatch path and the streaming
     # consumer so both behave identically.
    +# Path anchors: ``~/`` (Unix home-relative), ``/`` (Unix absolute),
    +# ``X:\\`` or ``X:/`` (Windows drive-letter absolute — #34632).
     MEDIA_TAG_CLEANUP_RE = re.compile(
         r'''[`"']?MEDIA:\s*'''
         r'''(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|'''
    -    r'''(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))'''
    +    r'''(?:~/|/|[A-Za-z]:[/\\])\S+(?:[^\S\n]+\S+)*?\.(?:''' + _MEDIA_EXT_ALTERNATION + r'''))'''
         r'''(?=[\s`"',;:)\]}]|$)[`"']?''',
         re.IGNORECASE,
     )
    ```

    ```diff
    @@ -2665,9 +2667,10 @@ def extract_local_files(content: str) -> Tuple[List[str], str]:
     
             # (?<![/:\w.]) prevents matching inside URLs (e.g. https://…/img.png)
             #             and relative paths (./foo.png)
    -        # (?:~/|/)    anchors to absolute or home-relative paths
    +        # (?:~/|/)    anchors to absolute or home-relative Unix paths
    +        # (?:[A-Za-z]:[/\\]) anchors to Windows drive-letter paths (#34632)
             path_re = re.compile(
    -            r'(?<![/:\w.])(?:~/|/)(?:[\w.\-]+/)*[\w.\-]+\.(?:' + ext_part + r')\b',
    +            r'(?<![/:\w.])(?:~/|/|[A-Za-z]:[/\\])(?:[\w.\-]+[/\\])*[\w.\-]+\.(?:' + ext_part + r')\b',
                 re.IGNORECASE,
             )
     
    ```
  - `gateway/run.py`

    ```diff
    @@ -17281,7 +17281,7 @@ def _clarify_callback_sync(question: str, choices) -> str:
                         _hc = _hm.get("content", "")
                         if "MEDIA:" in _hc:
                             _TOOL_MEDIA_RE = re.compile(
    -                            r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
    +                            r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
                                 r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
                                 r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
                                 r'txt|csv|apk|ipa))',
    ```

    ```diff
    @@ -17607,7 +17607,7 @@ def _approval_notify_sync(approval_data: dict) -> None:
                             content = msg.get("content", "")
                             if "MEDIA:" in content:
                                 _TOOL_MEDIA_RE = re.compile(
    -                                r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
    +                                r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
                                     r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
                                     r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
                                     r'txt|csv|apk|ipa))',
    ```
  - `tests/gateway/test_extract_local_files.py`

    ```diff
    @@ -336,9 +336,35 @@ def test_path_with_spaces_not_matched(self):
             paths, _ = _extract("File at /tmp/my file.png here")
             assert paths == []
     
    -    def test_windows_path_not_matched(self):
    -        """Windows-style paths should not match."""
    -        paths, _ = _extract("See C:\\Users\\test\\image.png")
    +    @pytest.mark.parametrize(
    +        "content,expected",
    +        [
    +            # Backslash separators (native Windows style)
    +            ("See C:\\Users\\test\\image.png here", "C:\\Users\\test\\image.png"),
    +            # Forward slashes with drive letter (common in cross-platform code)
    +            ("See C:/Users/test/image.png here", "C:/Users/test/image.png"),
    +            # Non-C: drive
    +            ("Video at D:/data/clip.mp4 ready", "D:/data/clip.mp4"),
    +            # Lowercase drive letter
    +            ("Path e:/audio/track.mp3 done", "e:/audio/track.mp3"),
    +        ],
    +    )
    +    def test_windows_drive_letter_paths_matched(self, content, expected):
    +        """Windows drive-letter paths (C:/..., C:\\...) must be detected (#34632).
    +
    +        Prior behavior anchored on (?:~/|/) only, which silently dropped
    +        Windows absolute paths so the agent's bare-path references were
    +        sent as text instead of native uploads.
    +        """
    +        paths, cleaned = _extract(content)
    +        assert paths == [expected]
    +        assert expected not in cleaned
    +
    +    def test_relative_windows_path_not_matched(self):
    +        """A bare Windows-style filename without a drive letter must still
    +        not match (e.g. ``foo\\bar.png`` is treated as relative, like its
    +        Unix sibling ``foo/bar.png``)."""
    +        paths, _ = _extract("File at foo\\bar.png here")
             assert paths == []
     
         def test_relative_path_not_matched(self):
    ```
  - `tests/gateway/test_platform_base.py`

    ```diff
    @@ -361,6 +361,45 @@ def test_both_directives_can_coexist(self):
             assert "[[audio_as_voice]]" not in cleaned
             assert "[[as_document]]" not in cleaned
     
    +    # Windows path support — regression coverage for #34632
    +
    +    def test_media_tag_windows_backslash_path(self):
    +        """extract_media should recognise Windows backslash paths."""
    +        media, cleaned = BasePlatformAdapter.extract_media(
    +            r"MEDIA:C:\Users\kotsu\file.pdf"
    +        )
    +        assert len(media) == 1
    +        assert media[0][0].endswith("file.pdf")
    +
    +    def test_media_tag_windows_forward_slash_path(self):
    +        """extract_media should recognise Windows forward-slash paths."""
    +        media, cleaned = BasePlatformAdapter.extract_media(
    +            "MEDIA:C:/Users/kotsu/file.pdf"
    +        )
    +        assert len(media) == 1
    +        assert media[0][0].endswith("file.pdf")
    +
    +    def test_media_tag_windows_drive_root(self):
    +        """extract_media should recognise a path at the drive root."""
    +        media, cleaned = BasePlatformAdapter.extract_media(
    +            r"MEDIA:D:\report.md"
    +        )
    +        assert len(media) == 1
    +        assert media[0][0].endswith("report.md")
    +
    +    def test_media_tag_unix_paths_still_work(self):
    +        """Unix absolute and tilde paths must still extract after Windows change."""
    +        for content in ["MEDIA:/tmp/audio.ogg", r"MEDIA:~/docs/notes.md"]:
    +            media, _ = BasePlatformAdapter.extract_media(content)
    +            assert len(media) == 1, f"Failed for: {content}"
    +
    +    def test_relative_path_still_ignored(self):
    +        """Relative Windows-style paths (no drive letter) must not match."""
    +        media, _ = BasePlatformAdapter.extract_media(
    +            r"MEDIA:Users\kotsu\file.pdf"
    +        )
    +        assert media == []
    +
     
     class TestMediaExtensionAllowlistParity:
         """Regression coverage for issue #34517 — the MEDIA: extension black hole.
    ```
  - `tests/gateway/test_run_tool_media_re.py`

    ```diff
    @@ -0,0 +1,147 @@
    +r"""Tests for _TOOL_MEDIA_RE regex patterns in gateway/run.py.
    +
    +Issue #34632: The _TOOL_MEDIA_RE patterns in GatewayRunner used (?:/|~\/) to
    +anchor paths, which only matched Unix-style absolute and home-relative paths.
    +Windows absolute paths (C:\\Users\\..., D:/...) were silently ignored, causing
    +MEDIA directive delivery to fail on Windows.
    +
    +Fix: Add [A-Za-z]:[/\\\\] as a third anchor alternative in both patterns.
    +
    +Two identical _TOOL_MEDIA_RE patterns exist in run.py:
    +1. History scanning (~L17223): collects already-seen media paths
    +2. Result scanning (~L17549): extracts new media tags from agent output
    +
    +This test file validates that both equivalent regex patterns correctly match
    +Windows paths while preserving existing Unix path matching behavior.
    +"""
    +
    +import re
    +
    +import pytest
    +
    +
    +# Reconstruct the exact _TOOL_MEDIA_RE pattern from gateway/run.py
    +# The pattern is built by concatenating raw string parts:
    +#   r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|...))'
    +_TOOL_MEDIA_RE = re.compile(
    +    r'MEDIA:((?:[A-Za-z]:[/\\]|/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
    +    r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
    +    r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
    +    r'txt|csv|apk|ipa))',
    +    re.IGNORECASE,
    +)
    +
    +
    +# Reconstruct the pre-fix pattern (without Windows anchor) for regression proof
    +_TOOL_MEDIA_RE_PRE_FIX = re.compile(
    +    r'MEDIA:((?:/|~\/)\S+\.(?:png|jpe?g|gif|webp|'
    +    r'mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|'
    +    r'flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|'
    +    r'txt|csv|apk|ipa))',
    +    re.IGNORECASE,
    +)
    +
    +
    +class TestToolMediaReWindowsPaths:
    +    """Issue #34632: _TOOL_MEDIA_RE must match Windows absolute paths."""
    +
    +    # ── Positive: Windows paths now match ──────────────────────────
    +
    +    @pytest.mark.parametrize("media_tag, expected_path", [
    +        # Windows backslash paths
    +        ("MEDIA:C:\\Users\\test\\image.png", "C:\\Users\\test\\image.png"),
    +        ("MEDIA:D:\\data\\report.pdf", "D:\\data\\report.pdf"),
    +        ("MEDIA:E:\\Photos\\vacation.jpg", "E:\\Photos\\vacation.jpg"),
    +        # Windows forward-slash paths
    +        ("MEDIA:C:/Users/test/image.png", "C:/Users/test/image.png"),
    +        ("MEDIA:D:/data/report.pdf", "D:/data/report.pdf"),
    +        # Mixed separators
    +        ("MEDIA:C:\\Users/test\\image.webp", "C:\\Users/test\\image.webp"),
    +        # Various extensions
    +        ("MEDIA:F:\\videos\\clip.mp4", "F:\\videos\\clip.mp4"),
    +        ("MEDIA:G:\\audio\\song.mp3", "G:\\audio\\song.mp3"),
    +        ("MEDIA:H:\\docs\\sheet.xlsx", "H:\\docs\\sheet.xlsx"),
    +        ("MEDIA:Z:\\archive\\backup.zip", "Z:\\archive\\backup.zip"),
    +    ])
    +    def test_windows_paths_match(self, media_tag, expected_path):
    +        """Windows absolute paths with drive letters are matched."""
    +        match = _TOOL_MEDIA_RE.search(media_tag)
    +        assert match is not None, f"Should match: {media_tag}"
    +        assert match.group(1) == expected_path
    +
    +    # ── Positive: Unix paths still match ───────────────────────────
    +
    +    @pytest.mark.parametrize("media_tag, expected_path", [
    +        ("MEDIA:/tmp/output.png", "/tmp/output.png"),
    +        ("MEDIA:/var/log/report.pdf", "/var/log/report.pdf"),
    +        ("MEDIA:/home/user/docs/file.txt", "/home/user/docs/file.txt"),
    +        # Home-relative
    +        ("MEDIA:~/Downloads/image.jpg", "~/Downloads/image.jpg"),
    +        ("MEDIA:~/Documents/report.pdf", "~/Documents/report.pdf"),
    +    ])
    +    def test_unix_paths_still_match(self, media_tag, expected_path):
    +        """Unix-style absolute and home-relative paths still match."""
    +        match = _TOOL_MEDIA_RE.search(media_tag)
    +        assert match is not None, f"Should match: {media_tag}"
    +        assert match.group(1) == expected_path
    +
    +    # ── Negative: invalid paths don't match ────────────────────────
    +
    +    @pytest.mark.parametrize("text", [
    +        "No MEDIA tag here",
    +        "MEDIA:relative/path/file.png",       # relative path, no anchor
    +        "MEDIA:file.png",                      # no directory
    +        "MEDIA:C:file.png",                    # drive letter but no separator
    +        "MEDIA:/path/to/file.unknown",         # unsupported extension
    +        "MEDIA:/path/to/file",                 # no extension
    +        "MEDIA:",                               # empty path
    +    ])
    +    def test_invalid_paths_dont_match(self, text):
    +        """Non-MEDIA text, relative paths, and unsupported extensions are ignored."""
    +        match = _TOOL_MEDIA_RE.search(text)
    +        assert match is None, f"Should NOT match: {text}"
    +
    +    # ── Negative/preserved: old pattern rejects Windows paths ──────
    +
    +    @pytest.mark.parametrize("media_tag", [
    +        "MEDIA:C:\\Users\\test\\image.png",
    +        "MEDIA:D:/data/report.pdf",
    +        "MEDIA:C:\\path\\file.jpg",
    +    ])
    +    def test_pre_fix_pattern_rejects_windows(self, media_tag):
    +        """The pre-fix pattern (without Windows anchor) does NOT match Windows paths.
    +        This proves the fix is necessary — without it, these paths are silently ignored."""
    +        match = _TOOL_MEDIA_RE_PRE_FIX.search(media_tag)
    +        assert match is None, f"Pre-fix pattern should NOT match: {media_tag}"
    +
    +    # ── Edge cases ─────────────────────────────────────────────────
    +
    +    def test_multiple_media_tags_in_content(self):
    +        """Multiple MEDIA tags in the same content are all found."""
    +        content = (
    +            "Some text MEDIA:C:\\path\\img.png and more MEDIA:/tmp/out.pdf trailing"
    +        )
    +        matches = list(_TOOL_MEDIA_RE.finditer(content))
    +        assert len(matches) == 2
    +        paths = [m.group(1) for m in matches]
    +        assert "C:\\path\\img.png" in paths
    +        assert "/tmp/out.pdf" in paths
    +
    +    def test_case_insensitive_drive_letter(self):
    +        """Drive letters are case-insensitive due to re.IGNORECASE."""
    +        match_lower = _TOOL_MEDIA_RE.search("MEDIA:c:\\path\\file.png")
    +        match_upper = _TOOL_MEDIA_RE.search("MEDIA:C:\\path\\file.png")
    +        assert match_lower is not None
    +        assert match_upper is not None
    +        assert match_lower.group(1).lower() == match_upper.group(1).lower()
    +
    +    @pytest.mark.parametrize("media_tag", [
    +        "MEDIA:C:\\path\\file.jpeg",
    +        "MEDIA:C:\\path\\file.JPG",
    +        "MEDIA:C:\\path\\file.GIF",
    +        "MEDIA:C:\\path\\file.MP4",
    +    ])
    +    def test_case_insensitive_extensions(self, media_tag):
    +        """File extensions are matched case-insensitively."""
    +        match = _TOOL_MEDIA_RE.search(media_tag)
    +        assert match is not None, f"Should match: {media_tag}"
    ```
- **#35328: fix(gateway): support Windows drive letters and backslashes in file path regex (fixes #35270)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#35388`, `#35161`

## Fix status

- Merged to main in PR #35388 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-30`.
- Subsystem: `gateway`
