Config rot: hermes config check now speaks up about renamed, uninstalled and disabled selections


You carried an old config.yaml to a new machine, or you uninstalled a plugin a few weeks ago and renamed a toolset. Hermes starts, chats, and looks perfectly healthy — until one day a platform bot stops answering, or a whole class of tools is simply missing. You open the config file and every name you expect is still sitting there.

This is config rot: the toolset names saved in platform_toolsets are whatever Hermes believed existed at save time. After a plugin is removed or a toolset is renamed, those names point at nothing, and the runtime silently ignores them. A patch merged 2026-09-29 (PR #127782, salvaging #127392 with the original authorship kept) makes hermes config check report the stale selections, and lines its judgement up with the runtime.

One resolver, three call sites

The structural core of the patch is collapsing “is this saved name still a real toolset?” into a single function:

# hermes_cli/toolset_validation.py
def saved_toolset_resolver(config: dict) -> Callable[[str], bool]:
    """is_valid_toolset for a saved platform_toolsets list, mirroring what
    tools_config._get_platform_tools lets through."""

Both hermes config check and hermes config migrate now go through it. Why does that matter? Because before the fix, migrate warned about correctly configured MCP server names and plugin-platform bundles — to the point of suggesting you change 'hermes-teams' into 'hermes-teams'. A migration check that argues with itself is pure noise.

The new rules are worth remembering, since they explain why your config stopped shouting:

  • A configured MCP server counts as known, even when it declares enabled: false. It is inactive, not a typo. The old code treated “inactive” as “unrecognised” and produced a pointless “unknown toolset — did you mean hermes-cli?”.
  • known_plugin_toolsets is no longer proof that a toolset exists. It records what a platform was offered at save time, so it hid exactly the case this check exists for: names left behind by uninstalled plugins.
  • Plugin lookup and the platform-manifest scan run only for names the cheaper checks cannot place. A clean config never triggers plugin discovery, so the check itself costs nothing.

Disabled platforms: judged the way the runtime sees them

The second family of false alarms came from plugins.disabled. The runtime is lenient about shape — it only honours a list — but the old check judged the raw YAML value, so this line:

plugins:
  disabled: platforms/telegram   # a string, ignored by the runtime

was reported as “platform disabled” while the runtime happily kept loading it. The check now reads through the runtime’s own reader (_get_disabled_plugins()), so a non-list value is no longer reported as disabling anything.

And when it does report, it checks whether the platform’s required credentials are actually configured:

platform plugin 'platforms/telegram' is disabled while its required credentials are configured.
Run `hermes plugins enable platforms/telegram` if you want it active.

Note “required”: the gate is _platform_manifest_env_entries(optional=False), so optional variables never count. A disabled platform with no token is left alone, and an optional env var cannot be mistaken for “you clearly meant to run this”.

A crash fixed, and one check deleted

Two more things that look unrelated but are not:

  • A malformed mcp_servers no longer kills the command. Write it as a list by accident (mcp_servers: [a]) and the old code raised an AttributeError traceback; now it runs and reports the problem as an ordinary diagnostic. A diagnostic command that dies is the least useful kind of failure.
  • The top-level toolsets check is gone. The original PR also validated the top-level toolsets key, but the CLI resolves toolsets from --toolsets or platform_toolsets, and reads the top-level key only for the legacy kanban opt-in. Checking a key the runtime never reads only invents work for you.

The table below comes from the patch’s own validation run (real hermes config check, temporary HERMES_HOME), old behaviour on the left:

Case Before Now
platform_toolsets.cli lists a removed name reported reported
toolset of an uninstalled plugin, remembered in known_plugin_toolsets silent reported
MCP server with enabled: false listed in platform_toolsets “unknown toolset, did you mean hermes-cli?” silent
mcp_servers: [a] (wrong shape) AttributeError traceback runs
plugins.disabled: platforms/telegram (string) with a token configured reported as disabled (runtime still loads it) silent
plugins.disabled: [platforms/telegram] with a token configured reported reported

Why your old config still parses

One historical wart is easy to miss: early hermes config set builds stored a bare JSON array argument as a string, so a config can contain this:

platform_toolsets:
  cli: '["browser", "terminal"]'    # a string, not a list

The runtime, hermes doctor, and hermes plugins enable now all read that section through one parser, so they agree on what you configured. When a single reader does its own compatibility shim, the other two end up seeing a different setup.

When to run it

Short version: after moving machines, uninstalling a plugin, renaming a toolset, or upgrading from an older build.

hermes config check      # report saved selections that no longer resolve
hermes config migrate    # migrate and repair using the same resolver
hermes tools             # interactively pick toolsets again
hermes plugins enable platforms/telegram   # enable a platform the check flagged

Three practical judgements:

  1. On “disabled while its required credentials are configured”, ask yourself whether you disabled it on purpose. The check itself allows for that — a configured credential is only a reason to mention it. If you want it live, run the suggested hermes plugins enable.
  2. On a toolset name that no longer resolves, don’t delete it immediately. Check whether it belonged to an uninstalled plugin; if you still want that capability, reinstall the plugin. Deleting the name only means reconfiguring it later.
  3. A clean config check is not a perfect config. It only judges whether saved selections can be honoured by the runtime — not whether your model is a good choice or your keys are current. Use hermes doctor for that class of question.

And this check only becomes useful if the config itself stays tidy: the MCP configuration guide and the plugin catalog write-up both make the same point — adding and removing plugins or MCP servers has to be followed by cleaning up the config. This patch moves that step out of your memory and into a command.

The short version

  • hermes config check now reports saved platform_toolsets names that no longer resolve, plus platforms disabled while their required credentials are configured.
  • It shares one resolver with hermes config migrate, ending self-contradictory warnings like suggesting 'hermes-teams' for 'hermes-teams'.
  • Judgement matches the runtime: a disabled MCP server is known, known_plugin_toolsets is not evidence, a non-list plugins.disabled no longer lies, optional env vars never count as required.
  • Plugin discovery runs only when needed, so a clean config pays nothing; a malformed mcp_servers no longer crashes the command.
  • The top-level toolsets key check was removed because the CLI never reads it (legacy kanban only).
  • Run it after moving machines, removing plugins, or upgrading, then repair with hermes config migrate and hermes tools.

Full usage on hermes config and hermes plugins.