{"repo":"NousResearch/hermes-agent","public_id":"26451","display_label":"Skip hidden directories in skill discovery","summary":"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.","subsystem":"skills","counts":{"members":4,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":0,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":390,"merged_at":"2026-03-05T03:24:01+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"stale","last_verified_at":"2026-03-05T03:24:01+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":389,"pr_number":390,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-05T03:24:01+00:00"}],"members":[{"kind":"issue","number":26451,"title":"Skip hidden workspace directories during skill discovery","url":null,"state":"open"},{"kind":"pr","number":390,"title":"fix: use Path.parts for hidden directory filter in skill listing","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/skill_commands.py","tests/tools/test_hidden_dir_filter.py","tools/skills_tool.py"],"fix_hunks":[{"file":"agent/skill_commands.py","hunks":["@@ -26,8 +26,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:\n         if not SKILLS_DIR.exists():\n             return _skill_commands\n         for skill_md in SKILLS_DIR.rglob(\"SKILL.md\"):\n-            path_str = str(skill_md)\n-            if '/.git/' in path_str or '/.github/' in path_str or '/.hub/' in path_str:\n+            if any(part in ('.git', '.github', '.hub') for part in skill_md.parts):\n                 continue\n             try:\n                 content = skill_md.read_text(encoding='utf-8')"]},{"file":"tests/tools/test_hidden_dir_filter.py","hunks":["@@ -0,0 +1,95 @@\n+\"\"\"Tests for the hidden directory filter in skills listing.\n+\n+Regression test: the original filter used hardcoded forward-slash strings\n+like '/.git/' which never match on Windows where Path uses backslashes.\n+This caused quarantined skills (.hub/quarantine/) to appear as installed.\n+\n+Now uses Path.parts which is platform-independent.\n+\"\"\"\n+\n+import os\n+from pathlib import Path, PurePosixPath, PureWindowsPath\n+\n+\n+def _old_filter_matches(path_str: str) -> bool:\n+    \"\"\"The BROKEN filter that used hardcoded forward slashes.\n+\n+    Returns True when the path SHOULD be filtered out.\n+    \"\"\"\n+    return '/.git/' in path_str or '/.github/' in path_str or '/.hub/' in path_str\n+\n+\n+def _new_filter_matches(path: Path) -> bool:\n+    \"\"\"The FIXED filter using Path.parts.\n+\n+    Returns True when the path SHOULD be filtered out.\n+    \"\"\"\n+    return any(part in ('.git', '.github', '.hub') for part in path.parts)\n+\n+\n+class TestOldFilterBrokenOnWindows:\n+    \"\"\"Demonstrate the bug: hardcoded '/' never matches Windows backslash paths.\"\"\"\n+\n+    def test_old_filter_misses_hub_on_windows_path(self):\n+        \"\"\"Old filter fails to catch .hub in a Windows-style path string.\"\"\"\n+        win_path = r\"C:\\Users\\me\\.hermes\\skills\\.hub\\quarantine\\evil-skill\\SKILL.md\"\n+        assert _old_filter_matches(win_path) is False  # Bug: should be True\n+\n+    def test_old_filter_misses_git_on_windows_path(self):\n+        \"\"\"Old filter fails to catch .git in a Windows-style path string.\"\"\"\n+        win_path = r\"C:\\Users\\me\\.hermes\\skills\\.git\\config\\SKILL.md\"\n+        assert _old_filter_matches(win_path) is False  # Bug: should be True\n+\n+    def test_old_filter_works_on_unix_path(self):\n+        \"\"\"Old filter works fine on Unix paths (the original platform).\"\"\"\n+        unix_path = \"/home/user/.hermes/skills/.hub/quarantine/evil-skill/SKILL.md\"\n+        assert _old_filter_matches(unix_path) is True\n+\n+\n+class TestNewFilterCrossPlatform:\n+    \"\"\"The fixed filter works on both Windows and Unix paths.\"\"\"\n+\n+    def test_hub_quarantine_filtered(self, tmp_path):\n+        \"\"\"A SKILL.md inside .hub/quarantine/ must be filtered out.\"\"\"\n+        p = tmp_path / \".hermes\" / \"skills\" / \".hub\" / \"quarantine\" / \"evil\" / \"SKILL.md\"\n+        assert _new_filter_matches(p) is True\n+\n+    def test_git_dir_filtered(self, tmp_path):\n+        \"\"\"A SKILL.md inside .git/ must be filtered out.\"\"\"\n+        p = tmp_path / \".hermes\" / \"skills\" / \".git\" / \"hooks\" / \"SKILL.md\"\n+        assert _new_filter_matches(p) is True\n+\n+    def test_github_dir_filtered(self, tmp_path):\n+        \"\"\"A SKILL.md inside .github/ must be filtered out.\"\"\"\n+        p = tmp_path / \".hermes\" / \"skills\" / \".github\" / \"workflows\" / \"SKILL.md\"\n+        assert _new_filter_matches(p) is True\n+\n+    def test_normal_skill_not_filtered(self, tmp_path):\n+        \"\"\"A regular skill SKILL.md must NOT be filtered out.\"\"\"\n+        p = tmp_path / \".hermes\" / \"skills\" / \"my-cool-skill\" / \"SKILL.md\"\n+        assert _new_filter_matches(p) is False\n+\n+    def test_nested_skill_not_filtered(self, tmp_path):\n+        \"\"\"A deeply nested regular skill must NOT be filtered out.\"\"\"\n+        p = tmp_path / \".hermes\" / \"skills\" / \"org\" / \"deep-skill\" / \"SKILL.md\"\n+        assert _new_filter_matches(p) is False\n+\n+    def test_dot_prefix_not_false_positive(self, tmp_path):\n+        \"\"\"A skill dir starting with dot but not in the filter list passes.\"\"\"\n+        p = tmp_path / \".hermes\" / \"skills\" / \".my-hidden-skill\" / \"SKILL.md\"\n+        assert _new_filter_matches(p) is False\n+\n+\n+class TestWindowsPathParts:\n+    \"\"\"Verify Path.parts correctly splits on the native separator.\"\"\"\n+\n+    def test_parts_contains_hidden_dir(self, tmp_path):\n+        \"\"\"Path.parts includes each directory component individually.\"\"\"\n+        p = tmp_path / \"skills\" / \".hub\" / \"quarantine\" / \"SKILL.md\"\n+        assert \".hub\" in p.parts\n+\n+    def test_parts_does_not_contain_combined_string(self, tmp_path):\n+        \"\"\"Path.parts splits by separator, not by substring.\"\"\"\n+        p = tmp_path / \"skills\" / \"my-hub-skill\" / \"SKILL.md\"\n+        # \".hub\" should NOT match \"my-hub-skill\" as a part\n+        assert \".hub\" not in p.parts"]},{"file":"tools/skills_tool.py","hunks":["@@ -196,8 +196,7 @@ def _find_all_skills() -> List[Dict[str, Any]]:\n         return skills\n     \n     for skill_md in SKILLS_DIR.rglob(\"SKILL.md\"):\n-        path_str = str(skill_md)\n-        if '/.git/' in path_str or '/.github/' in path_str or '/.hub/' in path_str:\n+        if any(part in ('.git', '.github', '.hub') for part in skill_md.parts):\n             continue\n             \n         skill_dir = skill_md.parent\n"]}]},{"kind":"pr","number":26458,"title":"fix(skills): skip hidden directories during discovery","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":26521,"title":"fix(skills): skip hidden directories during discovery","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26458","number":26458},{"kind":"pr","value":"#26451","number":26451}],"category":"duplicate_of","implication":"duplicate"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}