Your plugin stopped loading? The old import paths died on 2026-09-14

You wrote a small plugin, it worked for a month, and today it just stopped. After hermes update and a gateway restart the bot goes quiet, hermes plugins list says your plugin is not loaded, and the log contains a single ImportError. You haven’t touched a line of code.
This is not a flaky plugin system. It is a scheduled cleanup: the compatibility window closed. Here is why the layer existed, what exactly was removed, and how to get your plugin back.
1. September’s decomposition moved internal paths
In September 2026 Hermes split its large modules into <stem>_<topic> sibling files (PR #102117). Users barely notice. Plugins that import from internal paths notice immediately: the names they relied on are no longer defined where they used to be.
The project has always been explicit about this: internal import paths were never part of the plugin contract. The documented Native plugin compatibility contract protects behaviour — plugin.yaml, register(ctx), hook payloads, provider interfaces — not the location of a Python module.
But the decomposition would have broken existing plugins overnight, so a temporary compatibility layer shipped alongside it with a published expiry date: 2026-09-14. It did more than it sounds like:
- 328
PLUGIN-COMPATblocks appended to the affected modules; - 1,148 moved public names re-exposed through lazy
__getattr__pointer tables (lazy, to avoid import cycles); - 592 third-party or stdlib names restored, because the old module used to expose them incidentally;
- 290 public definitions restored verbatim that had been deleted as unused;
- 41 restored private helpers and 17 restored imports, purely to support the definitions above;
- 3 whole deleted modules stubbed out to re-export from their replacement;
- 34 names that could not be restored at all (leaked loop variables and the like), listed only for completeness.
Plugin authors had a dedicated checker: hermes plugins compat <path-to-your-plugin> printed every file:line, old path to new path, and exited 1 while anything remained. At runtime the first resolution through a shim raised a HermesPluginCompatWarning (a FutureWarning), once per name per process. Users who could not wait on an author had an escape hatch: plugins.allow_deprecated_imports: true.
2. What was removed
PR #126164 deleted the layer on 2026-09-28: 360 files, +38 / −27,749 lines. Gone with it:
- all 328
PLUGIN-COMPATblocks, plus the three re-export stub modules; - the tooling:
COMPAT_MANIFEST.md,compat_manifest.json,scripts/check_compat_pointers.pyand its CI lint step; - every reporting surface: the CLI banner notice,
hermes plugins compat, thehermes doctorsection, the post-update notice, the Desktop one-time dialog, and the loader’s pre-import skip; - the escape hatch itself —
plugins.allow_deprecated_importsno longer exists, and setting it does nothing.
Three inert stubs were deliberately kept. The reasoning is practical: an update that was already running pre-removal code may lazy-import these modules after the checkout swap, and deleting them outright would crash the updater. So hermes_cli/plugin_compat.py keeps three silent functions (compat_report returns an empty dict, removal_in_effect returns true, summary_lines returns an empty list) purely to survive that ordering.
3. What happens to your plugin
Short version: a plugin that still imports an old path cannot load. It fails with an ImportError, and hermes plugins list shows the reason. It isn’t “disabled” — it never started, which is why the bot simply goes silent.
Two ways to fix it, in order of preference:
- Use
ctxand the documented ABCs. Most “I need to import an internal function” needs are already covered byPluginContextmethods or a provider abstract base class. Code written that way survives a file reshuffle. - If you must import, import from the module that defines the name today. The full old-module to new-location map still exists — it is the manifest from the last commit that shipped the layer: COMPAT_MANIFEST.md (commit 5912ed81ed9). Update the import, restart the gateway, check the load status.
One boundary worth knowing: private names were never restored. If your plugin imported something like _foo, or monkeypatched an internal seam, it was never covered by the layer — even if you never saw a warning, it may already be broken. Those cases have to move to the public equivalent or to the new module.
4. Finding the exact line that broke
hermes plugins list # load status and reason, per plugin
hermes plugins show <name> # details for one plugin (including emits / listens)
hermes plugins doctor <path> # validate the plugin directory itself
The most direct test bypasses Hermes and imports the plugin in the same Python environment:
python -c "import your_plugin"
The ModuleNotFoundError: No module named 'agent.tools' (or whatever name appears) is the line to change. Restart the gateway afterwards — plugins load at process start.
5. What this change did not touch
If your plugin only uses documented surfaces, the removal is irrelevant to you:
plugin.yamlandregister(ctx): documentedPluginContextmethods are not removed or renamed; new parameters are optional, have defaults and are meant to be keyword-only; existing return fields are not removed or silently retyped.- Hook payloads: new data arrives as keyword fields only, never by changing the meaning or position of an existing one. A legacy callback receives the fields it declares; a callback with
**kwargsreceives the complete payload — which is why new plugins should always declare**kwargs. - Manifests: unknown
plugin.yamlfields are ignored, so an older Hermes can load a plugin carrying metadata from a newer release. - Provider interfaces: new methods have default implementations, and new callback context is forwarded only when signature inspection shows the provider accepts it.
- The deprecation policy still has four hard conditions: a replacement plus migration instructions, a warning at most once per process naming the earliest removal release, support for the old behaviour through at least two further minor releases, and behaviour-based compatibility coverage for both paths throughout that window.
The short version: the plugin contract is behaviour, not file paths. Write to the documentation and the next refactor won’t touch you.
If you are just starting out, begin with our plugin catalog guide; to run a memory backend, the memory provider migration write-up tells the other half of September’s story. Command-by-command details live in the hermes plugins reference, and for re-wiring plugins at runtime see the live rewire guide.