NousResearch/hermes-agent

Empty YAML section keys in CLI config handling

Cluster 58277 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #61733 on 10 Jul 2026. Check it covers your case.

The short answer

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

One bug report describes a TypeError in load_cli_config when a profile config.yaml has an empty terminal: key. The three related pull requests each propose handling empty or null config sections, such as keeping default dicts instead of replacing them. This summary does not state whether any change was merged or fixes the problem.

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.

  • #61733 fix(config): empty YAML section keys no longer replace default dicts (#58277)

    merged to main

    files: cli.py, hermes_cli/config.py, tests/hermes_cli/test_config.py, tests/hermes_cli/test_config_env_expansion.py

    Show the diff
    • cli.py
      @@ -543,6 +543,8 @@ def load_cli_config() -> Dict[str, Any]:
                       if key == "model":
                           continue  # Already handled above
                       if key in file_config:
      +                    if isinstance(defaults[key], dict) and file_config[key] is None:
      +                        continue
                           if isinstance(defaults[key], dict) and isinstance(file_config[key], dict):
                               defaults[key].update(file_config[key])
                           else:
    • hermes_cli/config.py
      @@ -6243,6 +6243,12 @@ def _deep_merge(base: dict, override: dict) -> dict:
           Keys in *override* take precedence. If both values are dicts the merge
           recurses, so a user who overrides only ``tts.elevenlabs.voice_id`` will
           keep the default ``tts.elevenlabs.model_id`` intact.
      +
      +    An empty section key in config.yaml (``terminal:`` with no value) parses
      +    as YAML ``None``; treating that as an override would replace the entire
      +    default dict with ``None`` and crash every downstream consumer that
      +    expects a mapping (#58277). A ``None`` override of a dict default is
      +    ignored — same as the key being absent.
           """
           result = base.copy()
           for key, value in override.items():
      @@ -6252,6 +6258,8 @@ def _deep_merge(base: dict, override: dict) -> dict:
                   and isinstance(value, dict)
               ):
                   result[key] = _deep_merge(result[key], value)
      +        elif key in result and isinstance(result[key], dict) and value is None:
      +            continue
               else:
                   result[key] = value
           return result
    • tests/hermes_cli/test_config.py
      @@ -360,6 +360,34 @@ def test_last_known_good_cached_no_rewarn_spam(self, tmp_path, capsys):
                   assert capsys.readouterr().err == ""
       
       
      +class TestEmptyConfigSections:
      +    """Empty section keys (``terminal:`` with no value) parse as YAML None
      +    and must not replace the default dict for that section (#58277)."""
      +
      +    def test_null_section_keeps_defaults_in_load_config(self, tmp_path):
      +        with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
      +            (tmp_path / "config.yaml").write_text(
      +                "model:\n  default: test/custom\n"
      +                "terminal:\n"
      +                "display:\n"
      +            )
      +            config = load_config()
      +            assert config["model"]["default"] == "test/custom"
      +            assert isinstance(config["terminal"], dict)
      +            assert config["terminal"] == DEFAULT_CONFIG["terminal"]
      +            assert isinstance(config["display"], dict)
      +
      +    def test_null_override_of_non_dict_default_still_applies(self, tmp_path):
      +        """None only shields dict defaults — explicit null for a scalar
      +        key remains an override (unchanged behavior)."""
      +        from hermes_cli.config import _deep_merge
      +
      +        merged = _deep_merge({"scalar": 5, "section": {"a": 1}},
      +                             {"scalar": None, "section": None})
      +        assert merged["scalar"] is None
      +        assert merged["section"] == {"a": 1}
      +
      +
       class TestSaveAndLoadRoundtrip:
           @staticmethod
           def _deny_config_reads(config_path):
    • tests/hermes_cli/test_config_env_expansion.py
      @@ -150,6 +150,18 @@ def test_unchanged_env_still_serves_cache(self, tmp_path, monkeypatch):
       class TestLoadCliConfigExpansion:
           """Verify that load_cli_config() also expands ${VAR} references."""
       
      +    def test_cli_config_ignores_empty_terminal_section(self, tmp_path, monkeypatch):
      +        config_file = tmp_path / "config.yaml"
      +        config_file.write_text("terminal:\n")
      +
      +        monkeypatch.setattr("cli._hermes_home", tmp_path)
      +
      +        from cli import load_cli_config
      +        config = load_cli_config()
      +
      +        assert isinstance(config["terminal"], dict)
      +        assert config["terminal"]["env_type"] == "local"
      +
           def test_cli_config_expands_auxiliary_api_key(self, tmp_path, monkeypatch):
               config_yaml = (
                   "auxiliary:\n"

2 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1

  • #58306 fix(cli): ignore empty config sections · merged via another change → bdecf0a, #61733
  • #58361 fix(cli): handle empty/null 'terminal:' YAML key in load_cli_config · done elsewhere → #58306, #61733
Closed issues · 1
  • Closed#58277[Bug]: Empty YAML key (terminal:) in profile config.yaml crashes load_cli_config with TypeError
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.50: 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
#61733
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
10 Jul 2026

For your agent

The same facts, typed.