hermes-plugin-development
Hermes Plugin Development
When to use
Load this skill when creating, modifying, debugging, reviewing, or releasing a standalone/native Hermes plugin. It especially applies when a plugin reads profile config or secrets, registers model tools or bundled skills, supports gateway multiplexing, or must load from HERMES_HOME/plugins/<slug> rather than from a development checkout.
Core design rules
- Keep plugin capability at the edge; do not patch Hermes core for plugin-specific behavior.
- Handlers return Hermes-compatible JSON strings unless the runtime contract explicitly requires an exception to propagate.
- Keep model-facing schemas minimal. Tenant/project/profile authority comes from the active runtime profile, never caller-supplied model arguments.
- Use Hermes-native profile resolution and credential scoping whenever available. Standalone fallbacks are compatibility paths, not alternate authority chains.
- Fail closed before network or filesystem mutation when required profile config, credentials, or scope is absent.
- Keep
plugin.yaml tool/skill declarations exactly synchronized with real registrations.
- Bundle skills using paths derived from the loaded package/plugin root; do not rely on the checkout working directory.
Strict-TDD workflow
- Inspect the plugin manifest, registration entry point, schemas, handlers, config/secret helpers, and existing real-loader test.
- Write one focused failing behavior test first and run it to record RED.
- For security boundaries, include hostile ambient state and cross-profile adversarial fixtures rather than only happy-path mocks.
- Make the smallest production change that turns the focused test GREEN.
- Repeat for each independent behavior; do not batch unobserved production fixes ahead of tests.
- Run the focused suite, then the complete plugin suite.
- Run a real temporary-
HERMES_HOME PluginManager.discover_and_load() smoke without adding the plugin directory to PYTHONPATH.
- Compare exact loader registrations against the manifest, inspect
git diff --check, stage only plugin-owned files, commit, and rerun post-commit verification.
Profile and multiplex safety
For any profile-aware plugin, treat the process environment as hostile ambient state under multiplexing:
- Active config/home must follow Hermes' ContextVar-scoped home resolver.
- Active credentials must follow Hermes' ContextVar-scoped secret resolver.
- An unscoped credential read in multiplex mode must remain a loud fail-closed error; generic handler wrappers must not swallow it.
- Non-secret routing values must come from the active profile's
config.yaml, not process-global plugin env vars.
- Missing Gate/service URLs and tenant/project references must never fall back to localhost or placeholders such as
current.
See references/multiplex-profile-isolation.md for the adversarial fixture, exact fallback rules, central tenant/project resolver pattern, and loader/manifest verification recipe.
Real loader compatibility gate
Copy the complete plugin into a temporary HERMES_HOME/plugins/<slug>, enable it in temporary config.yaml, and invoke the installed Hermes repository's real PluginManager. Set PYTHONPATH to Hermes only. Assert:
- plugin enabled with no loader error;
- exact expected tool names/count;
- exact qualified bundled skill names;
- representative handler behavior; and
- continued behavior across a second handler call.
A test-only PYTHONPATH=<plugin-dir> is invalid evidence because it can hide imports that fail under Hermes' namespaced directory loader.
Review checklist
- [ ] Test was observed failing before each production behavior change.
- [ ] No direct process-global profile routing under Hermes.
- [ ] No scoped-secret fallback after empty/error/unscoped reads.
- [ ] No model-selectable tenant/project authority.
- [ ] No localhost or
current tenant fallback.
- [ ] Required config fails before I/O with redacted errors.
- [ ] Tool wrappers preserve fail-closed scope exceptions.
- [ ] Manifest equals real registrations.
- [ ] Focused tests, full plugin suite, and real loader smoke pass.
- [ ] Final staged paths are plugin-owned and
git diff --check passes.
Common pitfalls
- Reading
HERMES_HOME directly and therefore ignoring the per-turn override.
- Reading secret env vars directly and leaking another profile's process-global value.
- Catching
UnscopedSecretError in a broad JSON error wrapper.
- Treating a default localhost URL as harmless configuration convenience.
- Duplicating project-reference fallback logic across tool modules.
- Verifying direct imports but never exercising Hermes' directory-plugin loader.
- Checking that one tool loaded while leaving
plugin.yaml stale for several others.