Fixes for null/scalar skills config parsing bugs

NousResearch/hermes-agent · cluster 13026 · hermes cli

Data as of

Nothing open on GitHub. Fix merged in #61797. Check it covers your case.

The short answer

0open issues
0open PRs
8items grouped
Known?
Yes. 8 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #61797. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #61797. That is separate from the fix event.
Still open
Nothing open on GitHub.

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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #61797 fix(cli): get_disabled_skills handles null and scalar skills config (#13026)

    merged to main

    files: hermes_cli/skills_config.py, tests/hermes_cli/test_skills_config.py

    Show the diff
    • hermes_cli/skills_config.py
      @@ -24,6 +24,23 @@
       
       # ─── Config Helpers ───────────────────────────────────────────────────────────
       
      +def _normalize_skill_names(values) -> Set[str]:
      +    """Normalize a config value into a set of skill names.
      +
      +    Mirrors ``agent.skill_utils._normalize_string_set``: ``None`` (YAML null)
      +    means empty, a bare scalar (``disabled: my-skill``) means a single-item
      +    list — NOT a set of its characters (#13026).
      +    """
      +    if values is None:
      +        return set()
      +    if isinstance(values, str):
      +        values = [values]
      +    try:
      +        return {str(v).strip() for v in values if str(v).strip()}
      +    except TypeError:
      +        return set()
      +
      +
       def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str]:
           """Return disabled skill names: the global list unioned with the
           platform-specific list when a platform is given.
      @@ -32,14 +49,16 @@ def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str
           platform list adds to the global list rather than replacing it. This
           mirrors ``agent.skill_utils.get_disabled_skill_names``.
           """
      -    skills_cfg = config.get("skills", {})
      -    global_disabled = set(skills_cfg.get("disabled", []))
      +    skills_cfg = config.get("skills") or {}
      +    if not isinstance(skills_cfg, dict):
      +        return set()
      +    global_disabled = _normalize_skill_names(skills_cfg.get("disabled"))
           if platform is None:
               return global_disabled
           platform_disabled = cfg_get(skills_cfg, "platform_disabled", platform)
           if platform_disabled is None:
               return global_disabled
      -    return global_disabled | set(platform_disabled)
      +    return global_disabled | _normalize_skill_names(platform_disabled)
       
       
       def save_disabled_skills(config: dict, disabled: Set[str], platform: Optional[str] = None):
    • tests/hermes_cli/test_skills_config.py
      @@ -46,6 +46,34 @@ def test_missing_skills_key(self):
               from hermes_cli.skills_config import get_disabled_skills
               assert get_disabled_skills({"other": "value"}) == set()
       
      +    def test_null_skills_section(self):
      +        """``skills:`` with no value (YAML null) must not crash (#13026)."""
      +        from hermes_cli.skills_config import get_disabled_skills
      +        assert get_disabled_skills({"skills": None}) == set()
      +        assert get_disabled_skills({"skills": None}, platform="telegram") == set()
      +
      +    def test_null_disabled_key(self):
      +        from hermes_cli.skills_config import get_disabled_skills
      +        assert get_disabled_skills({"skills": {"disabled": None}}) == set()
      +
      +    def test_scalar_disabled_is_single_skill_not_characters(self):
      +        """``disabled: my-skill`` (bare scalar) is one skill name, not a
      +        set of its characters (#13026)."""
      +        from hermes_cli.skills_config import get_disabled_skills
      +        assert get_disabled_skills({"skills": {"disabled": "my-skill"}}) == {"my-skill"}
      +
      +    def test_scalar_platform_disabled(self):
      +        from hermes_cli.skills_config import get_disabled_skills
      +        config = {"skills": {
      +            "disabled": ["global-skill"],
      +            "platform_disabled": {"telegram": "tg-skill"},
      +        }}
      +        assert get_disabled_skills(config, platform="telegram") == {"global-skill", "tg-skill"}
      +
      +    def test_non_dict_skills_section(self):
      +        from hermes_cli.skills_config import get_disabled_skills
      +        assert get_disabled_skills({"skills": "oops"}) == set()
      +
           def test_empty_disabled_list(self):
               from hermes_cli.skills_config import get_disabled_skills
               assert get_disabled_skills({"skills": {"disabled": []}}) == set()

5 PRs closed without merging

From the close comments: done elsewhere 4 · superseded by another PR 1

  • #13047 fix(cli): normalize disabled skills config parsing · done elsewhere → 4c03032, #61797, #13026, #13078, #50648
  • #13078 fix(skills_config): handle null/scalar values in get_disabled_skills · done elsewhere → #61797, #13026
  • #14094 fix(skills-config): normalize disabled skill entries · done elsewhere → #61797, #13078, #14094
  • #15501 fix: handle None skills config in skills_config helpers · done elsewhere → 4c03032, #61797, #13078
  • #50648 fix(cli): tolerate null/scalar values in get_disabled_skills · superseded by another PR
Closed issues · 2
  • Closed#13026[Bug]: get_disabled_skills crashes on `skills: null` and splits scalar names into characters
  • Closed#15486Bug: Web server returns 500 error when skills configuration returns None
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.26: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#61797
Closed issues
2
Attempts
5 PRs closed without merging
Fix link checked
10 Jul 2026

For your agent

The same facts, typed.