# NousResearch/hermes-agent · Bool coercion corrupting string enum config values

## Open now

- Nothing open on GitHub.

A bug where CLI config commands silently convert string values like 'off'/'on' to Python booleans, corrupting schema-validated enum settings. Multiple fix attempts address skipping bool coercion for string-typed or string-enum config keys.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #63499. Verify it addresses your case.
- **Recommended:** read PR #63499 (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-63499`. 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.40`: 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

- #47515: \[Bug\]: \`hermes config set\` silently coerces enum string values "off"/"on" to Python booleans, corrupting schema-validated enum settings

## Prior & competing PRs

- **#63499: fix(config): preserve string-typed config values**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/config.py`, `tests/hermes_cli/test_set_config_value.py`
  - fix hunks:
  - `hermes_cli/config.py`

    ```diff
    @@ -8097,6 +8097,20 @@ def edit_config():
         subprocess.run([editor, str(config_path)])
     
     
    +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``.
    +    """
    +    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
    +
    +
     def set_config_value(key: str, value: str):
         """Set a configuration value."""
         if is_managed():
    ```

    ```diff
    @@ -8154,16 +8168,21 @@ def set_config_value(key: str, value: str):
         # _set_nested which preserves list-typed nodes; before #17876 the
         # inline navigation here silently overwrote lists with dicts.
     
    -    # Convert value to appropriate type
    -    if value.lower() in {'true', 'yes', 'on'}:
    -        value = True
    -    elif value.lower() in {'false', 'no', 'off'}:
    -        value = False
    -    elif value.isdigit():
    -        value = int(value)
    -    elif value.replace('.', '', 1).isdigit():
    -        value = float(value)
    -
    +    # Preserve values for string-typed settings.  In particular, enum members
    +    # 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):
    +        if value.lower() in {'true', 'yes', 'on'}:
    +            coerced_value = True
    +        elif value.lower() in {'false', 'no', 'off'}:
    +            coerced_value = False
    +        elif value.isdigit():
    +            coerced_value = int(value)
    +        elif value.replace('.', '', 1).isdigit():
    +            coerced_value = float(value)
    +
    +    value = coerced_value
         _set_nested(user_config, key, value)
         # Normalize the api_base → base_url alias at set-time too (issue #8919),
         # so a fresh `hermes config set model.api_base ...` lands on the canonical
    ```
  - `tests/hermes_cli/test_set_config_value.py`

    ```diff
    @@ -249,6 +249,46 @@ def test_deeper_nesting_through_list(self, _isolated_hermes_home):
             assert allowlist[1] == {"name": "bob", "role": "admin"}
     
     
    +# ---------------------------------------------------------------------------
    +# String-typed config values — regression tests for #47515
    +# ---------------------------------------------------------------------------
    +
    +class TestStringTypedConfigValues:
    +    @pytest.mark.parametrize("value", ["off", "on", "yes", "no", "true", "false", "01"])
    +    def test_string_typed_values_are_not_coerced(self, _isolated_hermes_home, value):
    +        """Values stay strings when DEFAULT_CONFIG declares the leaf as a string."""
    +        set_config_value("approvals.mode", value)
    +
    +        import yaml
    +        saved = yaml.safe_load(_read_config(_isolated_hermes_home))
    +        assert saved["approvals"]["mode"] == value
    +        assert isinstance(saved["approvals"]["mode"], str)
    +
    +    @pytest.mark.parametrize("key, value, expected", [
    +        ("terminal.persistent_shell", "off", False),
    +        ("approvals.timeout", "30", 30),
    +    ])
    +    def test_non_string_defaults_keep_existing_coercion(
    +        self, _isolated_hermes_home, key, value, expected
    +    ):
    +        set_config_value(key, value)
    +
    +        import yaml
    +        saved = yaml.safe_load(_read_config(_isolated_hermes_home))
    +        node = saved
    +        for part in key.split("."):
    +            node = node[part]
    +        assert node == expected
    +        assert type(node) is type(expected)
    +
    +    def test_unknown_keys_keep_existing_coercion(self, _isolated_hermes_home):
    +        set_config_value("custom.enabled", "off")
    +
    +        import yaml
    +        saved = yaml.safe_load(_read_config(_isolated_hermes_home))
    +        assert saved["custom"]["enabled"] is False
    +
    +
     # ---------------------------------------------------------------------------
     # Secret redaction in display output (issue #50245)
     # ---------------------------------------------------------------------------
    ```
- **#47519: fix(cli): skip bool coercion for string-typed config keys**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#63499`
- **#47607: fix(config): skip bool coercion for string-enum config keys (#47515)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#63499`
- **#47618: fix(cli): skip bool coercion for string-typed config keys**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#63499`

## Fix status

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