{"repo":"NousResearch/hermes-agent","public_id":"58277","display_label":"Empty YAML section keys in CLI config handling","summary":"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.","subsystem":"hermes_cli","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":61733,"merged_at":"2026-07-10T01:23:19+00:00"},"resolution":{"state":"resolved","by":"pr-61733"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T01:23:19+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":58277,"pr_number":61733,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-10T01:23:19+00:00"}],"members":[{"kind":"issue","number":58277,"title":"[Bug]: Empty YAML key (terminal:) in profile config.yaml crashes load_cli_config with TypeError","url":null,"state":"closed"},{"kind":"pr","number":58306,"title":"fix(cli): ignore empty config sections","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"bdecf0ab9","number":0},{"kind":"pr","value":"#61733","number":61733}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":58361,"title":"fix(cli): handle empty/null 'terminal:' YAML key in load_cli_config","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#58306","number":58306},{"kind":"pr","value":"#61733","number":61733}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":61733,"title":"fix(config): empty YAML section keys no longer replace default dicts (#58277)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_cli/config.py","tests/hermes_cli/test_config.py","tests/hermes_cli/test_config_env_expansion.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -543,6 +543,8 @@ def load_cli_config() -> Dict[str, Any]:\n                 if key == \"model\":\n                     continue  # Already handled above\n                 if key in file_config:\n+                    if isinstance(defaults[key], dict) and file_config[key] is None:\n+                        continue\n                     if isinstance(defaults[key], dict) and isinstance(file_config[key], dict):\n                         defaults[key].update(file_config[key])\n                     else:"]},{"file":"hermes_cli/config.py","hunks":["@@ -6243,6 +6243,12 @@ def _deep_merge(base: dict, override: dict) -> dict:\n     Keys in *override* take precedence. If both values are dicts the merge\n     recurses, so a user who overrides only ``tts.elevenlabs.voice_id`` will\n     keep the default ``tts.elevenlabs.model_id`` intact.\n+\n+    An empty section key in config.yaml (``terminal:`` with no value) parses\n+    as YAML ``None``; treating that as an override would replace the entire\n+    default dict with ``None`` and crash every downstream consumer that\n+    expects a mapping (#58277). A ``None`` override of a dict default is\n+    ignored — same as the key being absent.\n     \"\"\"\n     result = base.copy()\n     for key, value in override.items():","@@ -6252,6 +6258,8 @@ def _deep_merge(base: dict, override: dict) -> dict:\n             and isinstance(value, dict)\n         ):\n             result[key] = _deep_merge(result[key], value)\n+        elif key in result and isinstance(result[key], dict) and value is None:\n+            continue\n         else:\n             result[key] = value\n     return result"]},{"file":"tests/hermes_cli/test_config.py","hunks":["@@ -360,6 +360,34 @@ def test_last_known_good_cached_no_rewarn_spam(self, tmp_path, capsys):\n             assert capsys.readouterr().err == \"\"\n \n \n+class TestEmptyConfigSections:\n+    \"\"\"Empty section keys (``terminal:`` with no value) parse as YAML None\n+    and must not replace the default dict for that section (#58277).\"\"\"\n+\n+    def test_null_section_keeps_defaults_in_load_config(self, tmp_path):\n+        with patch.dict(os.environ, {\"HERMES_HOME\": str(tmp_path)}):\n+            (tmp_path / \"config.yaml\").write_text(\n+                \"model:\\n  default: test/custom\\n\"\n+                \"terminal:\\n\"\n+                \"display:\\n\"\n+            )\n+            config = load_config()\n+            assert config[\"model\"][\"default\"] == \"test/custom\"\n+            assert isinstance(config[\"terminal\"], dict)\n+            assert config[\"terminal\"] == DEFAULT_CONFIG[\"terminal\"]\n+            assert isinstance(config[\"display\"], dict)\n+\n+    def test_null_override_of_non_dict_default_still_applies(self, tmp_path):\n+        \"\"\"None only shields dict defaults — explicit null for a scalar\n+        key remains an override (unchanged behavior).\"\"\"\n+        from hermes_cli.config import _deep_merge\n+\n+        merged = _deep_merge({\"scalar\": 5, \"section\": {\"a\": 1}},\n+                             {\"scalar\": None, \"section\": None})\n+        assert merged[\"scalar\"] is None\n+        assert merged[\"section\"] == {\"a\": 1}\n+\n+\n class TestSaveAndLoadRoundtrip:\n     @staticmethod\n     def _deny_config_reads(config_path):"]},{"file":"tests/hermes_cli/test_config_env_expansion.py","hunks":["@@ -150,6 +150,18 @@ def test_unchanged_env_still_serves_cache(self, tmp_path, monkeypatch):\n class TestLoadCliConfigExpansion:\n     \"\"\"Verify that load_cli_config() also expands ${VAR} references.\"\"\"\n \n+    def test_cli_config_ignores_empty_terminal_section(self, tmp_path, monkeypatch):\n+        config_file = tmp_path / \"config.yaml\"\n+        config_file.write_text(\"terminal:\\n\")\n+\n+        monkeypatch.setattr(\"cli._hermes_home\", tmp_path)\n+\n+        from cli import load_cli_config\n+        config = load_cli_config()\n+\n+        assert isinstance(config[\"terminal\"], dict)\n+        assert config[\"terminal\"][\"env_type\"] == \"local\"\n+\n     def test_cli_config_expands_auxiliary_api_key(self, tmp_path, monkeypatch):\n         config_yaml = (\n             \"auxiliary:\\n\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}