# NousResearch/hermes-agent · Fix hidden directory filter broken on Windows

## Open now

- Nothing open on GitHub.

A bug was identified where the hidden directory filter in skill listing used a hardcoded \`/\` separator, causing it to break on Windows. The fix replaces this with \`Path.parts\` for cross-platform compatibility.

## 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 #390. Verify it addresses your case.
- **Recommended:** read PR #390 (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-390`. 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

- #389: Hidden directory filter in skill listing uses hardcoded \`/\`, broken on Windows

## Prior & competing PRs

- **#390: fix: use Path.parts for hidden directory filter in skill listing**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/skill_commands.py`, `tests/tools/test_hidden_dir_filter.py`, `tools/skills_tool.py`
  - fix hunks:
  - `agent/skill_commands.py`

    ```diff
    @@ -26,8 +26,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
             if not SKILLS_DIR.exists():
                 return _skill_commands
             for skill_md in SKILLS_DIR.rglob("SKILL.md"):
    -            path_str = str(skill_md)
    -            if '/.git/' in path_str or '/.github/' in path_str or '/.hub/' in path_str:
    +            if any(part in ('.git', '.github', '.hub') for part in skill_md.parts):
                     continue
                 try:
                     content = skill_md.read_text(encoding='utf-8')
    ```
  - `tests/tools/test_hidden_dir_filter.py`

    ```diff
    @@ -0,0 +1,95 @@
    +"""Tests for the hidden directory filter in skills listing.
    +
    +Regression test: the original filter used hardcoded forward-slash strings
    +like '/.git/' which never match on Windows where Path uses backslashes.
    +This caused quarantined skills (.hub/quarantine/) to appear as installed.
    +
    +Now uses Path.parts which is platform-independent.
    +"""
    +
    +import os
    +from pathlib import Path, PurePosixPath, PureWindowsPath
    +
    +
    +def _old_filter_matches(path_str: str) -> bool:
    +    """The BROKEN filter that used hardcoded forward slashes.
    +
    +    Returns True when the path SHOULD be filtered out.
    +    """
    +    return '/.git/' in path_str or '/.github/' in path_str or '/.hub/' in path_str
    +
    +
    +def _new_filter_matches(path: Path) -> bool:
    +    """The FIXED filter using Path.parts.
    +
    +    Returns True when the path SHOULD be filtered out.
    +    """
    +    return any(part in ('.git', '.github', '.hub') for part in path.parts)
    +
    +
    +class TestOldFilterBrokenOnWindows:
    +    """Demonstrate the bug: hardcoded '/' never matches Windows backslash paths."""
    +
    +    def test_old_filter_misses_hub_on_windows_path(self):
    +        """Old filter fails to catch .hub in a Windows-style path string."""
    +        win_path = r"C:\Users\me\.hermes\skills\.hub\quarantine\evil-skill\SKILL.md"
    +        assert _old_filter_matches(win_path) is False  # Bug: should be True
    +
    +    def test_old_filter_misses_git_on_windows_path(self):
    +        """Old filter fails to catch .git in a Windows-style path string."""
    +        win_path = r"C:\Users\me\.hermes\skills\.git\config\SKILL.md"
    +        assert _old_filter_matches(win_path) is False  # Bug: should be True
    +
    +    def test_old_filter_works_on_unix_path(self):
    +        """Old filter works fine on Unix paths (the original platform)."""
    +        unix_path = "/home/user/.hermes/skills/.hub/quarantine/evil-skill/SKILL.md"
    +        assert _old_filter_matches(unix_path) is True
    +
    +
    +class TestNewFilterCrossPlatform:
    +    """The fixed filter works on both Windows and Unix paths."""
    +
    +    def test_hub_quarantine_filtered(self, tmp_path):
    +        """A SKILL.md inside .hub/quarantine/ must be filtered out."""
    +        p = tmp_path / ".hermes" / "skills" / ".hub" / "quarantine" / "evil" / "SKILL.md"
    +        assert _new_filter_matches(p) is True
    +
    +    def test_git_dir_filtered(self, tmp_path):
    +        """A SKILL.md inside .git/ must be filtered out."""
    +        p = tmp_path / ".hermes" / "skills" / ".git" / "hooks" / "SKILL.md"
    +        assert _new_filter_matches(p) is True
    +
    +    def test_github_dir_filtered(self, tmp_path):
    +        """A SKILL.md inside .github/ must be filtered out."""
    +        p = tmp_path / ".hermes" / "skills" / ".github" / "workflows" / "SKILL.md"
    +        assert _new_filter_matches(p) is True
    +
    +    def test_normal_skill_not_filtered(self, tmp_path):
    +        """A regular skill SKILL.md must NOT be filtered out."""
    +        p = tmp_path / ".hermes" / "skills" / "my-cool-skill" / "SKILL.md"
    +        assert _new_filter_matches(p) is False
    +
    +    def test_nested_skill_not_filtered(self, tmp_path):
    +        """A deeply nested regular skill must NOT be filtered out."""
    +        p = tmp_path / ".hermes" / "skills" / "org" / "deep-skill" / "SKILL.md"
    +        assert _new_filter_matches(p) is False
    +
    +    def test_dot_prefix_not_false_positive(self, tmp_path):
    +        """A skill dir starting with dot but not in the filter list passes."""
    +        p = tmp_path / ".hermes" / "skills" / ".my-hidden-skill" / "SKILL.md"
    +        assert _new_filter_matches(p) is False
    +
    +
    +class TestWindowsPathParts:
    +    """Verify Path.parts correctly splits on the native separator."""
    +
    +    def test_parts_contains_hidden_dir(self, tmp_path):
    +        """Path.parts includes each directory component individually."""
    +        p = tmp_path / "skills" / ".hub" / "quarantine" / "SKILL.md"
    +        assert ".hub" in p.parts
    +
    +    def test_parts_does_not_contain_combined_string(self, tmp_path):
    +        """Path.parts splits by separator, not by substring."""
    +        p = tmp_path / "skills" / "my-hub-skill" / "SKILL.md"
    +        # ".hub" should NOT match "my-hub-skill" as a part
    +        assert ".hub" not in p.parts
    ```
  - `tools/skills_tool.py`

    ```diff
    @@ -196,8 +196,7 @@ def _find_all_skills() -> List[Dict[str, Any]]:
             return skills
         
         for skill_md in SKILLS_DIR.rglob("SKILL.md"):
    -        path_str = str(skill_md)
    -        if '/.git/' in path_str or '/.github/' in path_str or '/.hub/' in path_str:
    +        if any(part in ('.git', '.github', '.hub') for part in skill_md.parts):
                 continue
                 
             skill_dir = skill_md.parent
    ```
- **#401: Fix hidden directory filter broken on Windows**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#390`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._

## Fix status

- Merged to main in PR #390 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-03-05`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
