# NousResearch/hermes-agent · Skip hidden directories in skill discovery

## Open now

- 1 issue, 0 PRs open on GitHub.
- #26451: Skip hidden workspace directories during skill discovery

These issues and pull requests address filtering out hidden workspace directories during skill discovery. Multiple fixes were proposed, including using Path.parts for the hidden directory check.

## TL;DR

- 4 member(s): 1 open issue(s), 0 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 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: `unresolved`. 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.50`: 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

- #26451: Skip hidden workspace directories during skill discovery

## 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
    ```
- **#26458: fix(skills): skip hidden directories during discovery**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#26521: fix(skills): skip hidden directories during discovery**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#26458`, `#26451`

## 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.
- Subsystem: `skills`
