{"repo":"NousResearch/hermes-agent","public_id":"13026","display_label":"Fixes for null/scalar skills config parsing bugs","summary":"Multiple fixes addressing crashes and incorrect behavior when the skills configuration is null, None, or a scalar value instead of the expected list or dict. Affects both CLI parsing and web server handling of disabled skills.","subsystem":"hermes_cli","counts":{"members":8,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":5},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":61797},"resolution":{"state":"partially_resolved","by":"pr-61797"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T02:57:54+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.26285714285714284},"fix_edges":[{"issue_number":13026,"pr_number":61797,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":15486,"pr_number":61797,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":13026,"title":"[Bug]: get_disabled_skills crashes on `skills: null` and splits scalar names into characters","url":null,"state":"closed"},{"kind":"issue","number":15486,"title":"Bug: Web server returns 500 error when skills configuration returns None","url":null,"state":"closed"},{"kind":"pr","number":13047,"title":"fix(cli): normalize disabled skills config parsing","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4c03032a240a297ce6603bf352cf76d88f59d75f","number":0},{"kind":"pr","value":"#61797","number":61797},{"kind":"pr","value":"#61797","number":61797},{"kind":"pr","value":"#13026","number":13026},{"kind":"pr","value":"#13078","number":13078},{"kind":"pr","value":"#50648","number":50648},{"kind":"pr","value":"#61797","number":61797}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":13078,"title":"fix(skills_config): handle null/scalar values in get_disabled_skills","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61797","number":61797},{"kind":"pr","value":"#13026","number":13026}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":14094,"title":"fix(skills-config): normalize disabled skill entries","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61797","number":61797},{"kind":"pr","value":"#13078","number":13078},{"kind":"pr","value":"#14094","number":14094}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":15501,"title":"fix: handle None skills config in skills_config helpers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4c03032a240a297ce6603bf352cf76d88f59d75f","number":0},{"kind":"pr","value":"#61797","number":61797},{"kind":"pr","value":"#13078","number":13078},{"kind":"pr","value":"#13078","number":13078},{"kind":"pr","value":"#61797","number":61797}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":50648,"title":"fix(cli): tolerate null/scalar values in get_disabled_skills","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":61797,"title":"fix(cli): get_disabled_skills handles null and scalar skills config (#13026)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/skills_config.py","tests/hermes_cli/test_skills_config.py"],"fix_hunks":[{"file":"hermes_cli/skills_config.py","hunks":["@@ -24,6 +24,23 @@\n \n # ─── Config Helpers ───────────────────────────────────────────────────────────\n \n+def _normalize_skill_names(values) -> Set[str]:\n+    \"\"\"Normalize a config value into a set of skill names.\n+\n+    Mirrors ``agent.skill_utils._normalize_string_set``: ``None`` (YAML null)\n+    means empty, a bare scalar (``disabled: my-skill``) means a single-item\n+    list — NOT a set of its characters (#13026).\n+    \"\"\"\n+    if values is None:\n+        return set()\n+    if isinstance(values, str):\n+        values = [values]\n+    try:\n+        return {str(v).strip() for v in values if str(v).strip()}\n+    except TypeError:\n+        return set()\n+\n+\n def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str]:\n     \"\"\"Return disabled skill names: the global list unioned with the\n     platform-specific list when a platform is given.","@@ -32,14 +49,16 @@ def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str\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+    skills_cfg = config.get(\"skills\") or {}\n+    if not isinstance(skills_cfg, dict):\n+        return set()\n+    global_disabled = _normalize_skill_names(skills_cfg.get(\"disabled\"))\n     if platform is None:\n         return global_disabled\n     platform_disabled = cfg_get(skills_cfg, \"platform_disabled\", platform)\n     if platform_disabled is None:\n         return global_disabled\n-    return global_disabled | set(platform_disabled)\n+    return global_disabled | _normalize_skill_names(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":["@@ -46,6 +46,34 @@ def test_missing_skills_key(self):\n         from hermes_cli.skills_config import get_disabled_skills\n         assert get_disabled_skills({\"other\": \"value\"}) == set()\n \n+    def test_null_skills_section(self):\n+        \"\"\"``skills:`` with no value (YAML null) must not crash (#13026).\"\"\"\n+        from hermes_cli.skills_config import get_disabled_skills\n+        assert get_disabled_skills({\"skills\": None}) == set()\n+        assert get_disabled_skills({\"skills\": None}, platform=\"telegram\") == set()\n+\n+    def test_null_disabled_key(self):\n+        from hermes_cli.skills_config import get_disabled_skills\n+        assert get_disabled_skills({\"skills\": {\"disabled\": None}}) == set()\n+\n+    def test_scalar_disabled_is_single_skill_not_characters(self):\n+        \"\"\"``disabled: my-skill`` (bare scalar) is one skill name, not a\n+        set of its characters (#13026).\"\"\"\n+        from hermes_cli.skills_config import get_disabled_skills\n+        assert get_disabled_skills({\"skills\": {\"disabled\": \"my-skill\"}}) == {\"my-skill\"}\n+\n+    def test_scalar_platform_disabled(self):\n+        from hermes_cli.skills_config import get_disabled_skills\n+        config = {\"skills\": {\n+            \"disabled\": [\"global-skill\"],\n+            \"platform_disabled\": {\"telegram\": \"tg-skill\"},\n+        }}\n+        assert get_disabled_skills(config, platform=\"telegram\") == {\"global-skill\", \"tg-skill\"}\n+\n+    def test_non_dict_skills_section(self):\n+        from hermes_cli.skills_config import get_disabled_skills\n+        assert get_disabled_skills({\"skills\": \"oops\"}) == set()\n+\n     def test_empty_disabled_list(self):\n         from hermes_cli.skills_config import get_disabled_skills\n         assert get_disabled_skills({\"skills\": {\"disabled\": []}}) == set()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}