diff --git a/hermes_cli/config.py b/hermes_cli/config.py index ac3b0aaf82fc..f6950ef1b1a4 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -8355,15 +8355,16 @@ def edit_config(): def _default_value_for_key(dotted_key: str): """Return the leaf value declared for *dotted_key* in ``DEFAULT_CONFIG``. - Unknown keys and non-leaf paths return ``None`` so they retain the legacy - best-effort coercion used by ``config set``. + Unknown paths return ``None`` so they retain the legacy best-effort + coercion used by ``config set``. Container defaults are returned too so + the setter can preserve their declared list or mapping type. """ node = DEFAULT_CONFIG for part in dotted_key.split("."): if not isinstance(node, dict) or part not in node: return None node = node[part] - return node if not isinstance(node, dict) else None + return node def set_config_value(key: str, value: str): @@ -8415,7 +8416,15 @@ def set_config_value(key: str, value: str): # such as approvals.mode="off" must not become YAML booleans. Unknown keys # retain the historical best-effort coercion behavior. coerced_value: Any = value - if not isinstance(_default_value_for_key(key), str): + default_value = _default_value_for_key(key) + if isinstance(default_value, (dict, list)): + try: + parsed_value = yaml.safe_load(value) + except yaml.YAMLError: + parsed_value = None + if isinstance(parsed_value, type(default_value)): + coerced_value = parsed_value + elif not isinstance(default_value, str): if value.lower() in {'true', 'yes', 'on'}: coerced_value = True elif value.lower() in {'false', 'no', 'off'}: diff --git a/tests/hermes_cli/test_set_config_value.py b/tests/hermes_cli/test_set_config_value.py index 135223afb974..a82ff0b68340 100644 --- a/tests/hermes_cli/test_set_config_value.py +++ b/tests/hermes_cli/test_set_config_value.py @@ -96,6 +96,26 @@ def test_terminal_ssh_prefix_routes_to_env(self, _isolated_hermes_home): class TestConfigYamlRouting: """Regular config keys should go to config.yaml, NOT .env.""" + def test_list_default_parses_yaml_list_literal(self, _isolated_hermes_home): + """A list-valued default accepts a shell-friendly YAML list.""" + set_config_value("command_allowlist", "['git', 'ollama', 'brew']") + + import yaml + + saved = yaml.safe_load(_read_config(_isolated_hermes_home)) + assert saved["command_allowlist"] == ["git", "ollama", "brew"] + assert isinstance(saved["command_allowlist"], list) + + def test_mapping_default_parses_yaml_mapping_literal(self, _isolated_hermes_home): + """A mapping-valued default accepts a shell-friendly YAML mapping.""" + set_config_value("quick_commands", "{hello: {type: exec}}") + + import yaml + + saved = yaml.safe_load(_read_config(_isolated_hermes_home)) + assert saved["quick_commands"] == {"hello": {"type": "exec"}} + assert isinstance(saved["quick_commands"], dict) + def test_simple_key(self, _isolated_hermes_home): set_config_value("model", "gpt-4o") config = _read_config(_isolated_hermes_home)