{"repo":"NousResearch/hermes-agent","public_id":"46201","display_label":"Platform-disabled skills appearing in <available_skills> prompt","summary":"All four members concern disabled skills still showing up in the <available_skills> prompt; one is a bug report that also mentions false loads, and three reference #46201 with proposed fixes. One of them also mentions unifying resolution sites. Their status is not established here.","subsystem":"skills","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":46236,"merged_at":"2026-06-14T17:30:12+00:00"},"resolution":{"state":"resolved","by":"pr-46236"},"freshness":{"state":"verified","last_verified_at":"2026-06-14T17:30:12+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":46201,"pr_number":46236,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-14T17:30:12+00:00"}],"members":[{"kind":"issue","number":46201,"title":"[Bug]: Disabled skills still appear in <available_skills> prompt and trigger false loads","url":null,"state":"closed"},{"kind":"pr","number":46203,"title":"fix: platform-disabled skills still appear in <available_skills> prompt (#46201)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7bbe7024c207a6dad982967780995ad18ac7e7be","number":0},{"kind":"pr","value":"#46201","number":46201},{"kind":"pr","value":"#46236","number":46236}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":46220,"title":"fix(skills): filter platform-disabled skills from prompt (#46201)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"7bbe7024c207a6dad982967780995ad18ac7e7be","number":0},{"kind":"commit","value":"ce19fdb7ce27d94462a650ade8a2b94882590a38","number":0},{"kind":"pr","value":"#46236","number":46236}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":46236,"title":"fix(skills): platform-disabled skills still appear in <available_skills> + unify all resolution sites (#46201)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/prompt_builder.py","agent/skill_utils.py","hermes_cli/skills_config.py","tests/hermes_cli/test_skills_config.py","tools/skills_tool.py"],"fix_hunks":[{"file":"agent/prompt_builder.py","hunks":["@@ -1164,7 +1164,7 @@ def build_skills_system_prompt(\n         or get_session_env(\"HERMES_SESSION_PLATFORM\")\n         or \"\"\n     )\n-    disabled = get_disabled_skill_names()\n+    disabled = get_disabled_skill_names(_platform_hint or None)\n     cache_key = (\n         str(skills_dir.resolve()),\n         tuple(str(d) for d in external_dirs),"]},{"file":"agent/skill_utils.py","hunks":["@@ -278,8 +278,10 @@ def get_disabled_skill_names(platform: str | None = None) -> Set[str]:\n     Args:\n         platform: Explicit platform name (e.g. ``\"telegram\"``).  When\n             *None*, resolves from ``HERMES_PLATFORM`` or\n-            ``HERMES_SESSION_PLATFORM`` env vars.  Falls back to the\n-            global disabled list when no platform is determined.\n+            ``HERMES_SESSION_PLATFORM`` env vars.  Returns the global\n+            disabled list, unioned with the platform-specific list when a\n+            platform is resolved (a globally-disabled skill stays disabled\n+            on every platform).\n \n     Reads the config file directly (no CLI config imports) to stay\n     lightweight.","@@ -305,13 +307,14 @@ def get_disabled_skill_names(platform: str | None = None) -> Set[str]:\n         or os.getenv(\"HERMES_PLATFORM\")\n         or get_session_env(\"HERMES_SESSION_PLATFORM\")\n     )\n+    global_disabled = _normalize_string_set(skills_cfg.get(\"disabled\"))\n     if resolved_platform:\n         platform_disabled = (skills_cfg.get(\"platform_disabled\") or {}).get(\n             resolved_platform\n         )\n         if platform_disabled is not None:\n-            return _normalize_string_set(platform_disabled)\n-    return _normalize_string_set(skills_cfg.get(\"disabled\"))\n+            return global_disabled | _normalize_string_set(platform_disabled)\n+    return global_disabled\n \n \n def _normalize_string_set(values) -> Set[str]:"]},{"file":"hermes_cli/skills_config.py","hunks":["@@ -25,7 +25,13 @@\n # ─── Config Helpers ───────────────────────────────────────────────────────────\n \n def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str]:\n-    \"\"\"Return disabled skill names. Platform-specific list falls back to global.\"\"\"\n+    \"\"\"Return disabled skill names: the global list unioned with the\n+    platform-specific list when a platform is given.\n+\n+    A globally-disabled skill stays disabled on every platform, so the\n+    platform list adds to the global list rather than replacing it. This\n+    mirrors ``agent.skill_utils.get_disabled_skill_names``.\n+    \"\"\"\n     skills_cfg = config.get(\"skills\", {})\n     global_disabled = set(skills_cfg.get(\"disabled\", []))\n     if platform is None:","@@ -33,7 +39,7 @@ def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str\n     platform_disabled = cfg_get(skills_cfg, \"platform_disabled\", platform)\n     if platform_disabled is None:\n         return global_disabled\n-    return set(platform_disabled)\n+    return global_disabled | set(platform_disabled)\n \n \n def save_disabled_skills(config: dict, disabled: Set[str], platform: Optional[str] = None):"]},{"file":"tests/hermes_cli/test_skills_config.py","hunks":["@@ -22,7 +22,19 @@ def test_reads_platform_disabled(self):\n             \"disabled\": [\"skill-a\"],\n             \"platform_disabled\": {\"telegram\": [\"skill-b\"]}\n         }}\n-        assert get_disabled_skills(config, platform=\"telegram\") == {\"skill-b\"}\n+        # Union of global + platform: a globally-disabled skill stays disabled\n+        # on every platform, and the platform list adds to it.\n+        assert get_disabled_skills(config, platform=\"telegram\") == {\"skill-a\", \"skill-b\"}\n+\n+    def test_platform_list_unions_with_global(self):\n+        from hermes_cli.skills_config import get_disabled_skills\n+        config = {\"skills\": {\n+            \"disabled\": [\"global-skill\"],\n+            \"platform_disabled\": {\"telegram\": []}\n+        }}\n+        # An explicit empty platform list does NOT re-enable a globally-disabled\n+        # skill (matches issue #46201 — global disables hold everywhere).\n+        assert get_disabled_skills(config, platform=\"telegram\") == {\"global-skill\"}\n \n     def test_platform_falls_back_to_global(self):\n         from hermes_cli.skills_config import get_disabled_skills","@@ -102,14 +114,27 @@ def test_platform_disabled(self, mock_load):\n         assert _is_skill_disabled(\"tg-skill\", platform=\"telegram\") is True\n \n     @patch(\"hermes_cli.config.load_config\")\n-    def test_platform_enabled_overrides_global(self, mock_load):\n+    def test_globally_disabled_stays_disabled_on_platform(self, mock_load):\n+        mock_load.return_value = {\"skills\": {\n+            \"disabled\": [\"skill-a\"],\n+            \"platform_disabled\": {\"telegram\": [\"tg-skill\"]}\n+        }}\n+        from tools.skills_tool import _is_skill_disabled\n+        # Union: a globally-disabled skill stays disabled on a platform that\n+        # has its own platform_disabled list (matches issue #46201).\n+        assert _is_skill_disabled(\"skill-a\", platform=\"telegram\") is True\n+        assert _is_skill_disabled(\"tg-skill\", platform=\"telegram\") is True\n+\n+    @patch(\"hermes_cli.config.load_config\")\n+    def test_empty_platform_list_keeps_global_disabled(self, mock_load):\n         mock_load.return_value = {\"skills\": {\n             \"disabled\": [\"skill-a\"],\n             \"platform_disabled\": {\"telegram\": []}\n         }}\n         from tools.skills_tool import _is_skill_disabled\n-        # telegram has explicit empty list -> skill-a is NOT disabled for telegram\n-        assert _is_skill_disabled(\"skill-a\", platform=\"telegram\") is False\n+        # An explicit empty platform list does NOT re-enable a globally-disabled\n+        # skill — global disables hold on every platform.\n+        assert _is_skill_disabled(\"skill-a\", platform=\"telegram\") is True\n \n     @patch(\"hermes_cli.config.load_config\")\n     def test_platform_falls_back_to_global(self, mock_load):","@@ -164,7 +189,7 @@ def test_explicit_platform_param(self, tmp_path, monkeypatch):\n \n         from agent.skill_utils import get_disabled_skill_names\n         result = get_disabled_skill_names(platform=\"telegram\")\n-        assert result == {\"tg-only-skill\"}\n+        assert result == {\"tg-only-skill\", \"global-skill\"}\n \n     def test_session_platform_env_var(self, tmp_path, monkeypatch):\n         \"\"\"HERMES_SESSION_PLATFORM should be used when HERMES_PLATFORM is unset.\"\"\"","@@ -183,7 +208,7 @@ def test_session_platform_env_var(self, tmp_path, monkeypatch):\n \n         from agent.skill_utils import get_disabled_skill_names\n         result = get_disabled_skill_names()\n-        assert result == {\"discord-skill\"}\n+        assert result == {\"discord-skill\", \"global-skill\"}\n \n     def test_hermes_platform_takes_precedence(self, tmp_path, monkeypatch):\n         \"\"\"HERMES_PLATFORM should win over HERMES_SESSION_PLATFORM.\"\"\""]},{"file":"tools/skills_tool.py","hunks":["@@ -583,11 +583,15 @@ def _is_skill_disabled(name: str, platform: str = None) -> bool:\n         config = load_config()\n         skills_cfg = config.get(\"skills\", {})\n         resolved_platform = platform or os.getenv(\"HERMES_PLATFORM\") or _get_session_platform()\n+        global_disabled = skills_cfg.get(\"disabled\", [])\n         if resolved_platform:\n             platform_disabled = cfg_get(skills_cfg, \"platform_disabled\", resolved_platform)\n             if platform_disabled is not None:\n-                return name in platform_disabled\n-        return name in skills_cfg.get(\"disabled\", [])\n+                # A globally-disabled skill stays disabled on every platform;\n+                # the platform list adds to it rather than replacing it. Keep\n+                # in sync with agent.skill_utils.get_disabled_skill_names.\n+                return name in platform_disabled or name in global_disabled\n+        return name in global_disabled\n     except Exception:\n         return False\n \n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}