# NousResearch/hermes-agent · Empty YAML section keys in CLI config handling

## Open now

- 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.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #61733. Verify it addresses your case.
- **Recommended:** read PR #61733 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-61733`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member 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.

## Closed / duplicate issues

- #58277: \[Bug\]: Empty YAML key (terminal:) in profile config.yaml crashes load_cli_config with TypeError

## Prior & competing PRs

- **#61733: fix(config): empty YAML section keys no longer replace default dicts (#58277)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_cli/config.py`, `tests/hermes_cli/test_config.py`, `tests/hermes_cli/test_config_env_expansion.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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():
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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"
    ```
- **#58306: fix(cli): ignore empty config sections**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `bdecf0ab9`, `#61733`
- **#58361: fix(cli): handle empty/null 'terminal:' YAML key in load_cli_config**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#58306`, `#61733`

## Fix status

- Merged to main in PR #61733 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-10`.
- Subsystem: `hermes_cli`
