Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom Plugins

11.7. Custom Plugins — The Deepest Extension Layer

Custom Plugins — the deepest extension layer. When MCP and the ready-made channels are not enough — you need a completely custom tool, want to intervene in the agent loop, or want to add a whole new gateway platform — this is the layer that makes it possible without forking Hermes.

Minimal structure

~/.hermes/plugins/hello-world/
├── plugin.yaml       # manifest: name, description
└── __init__.py       # register(ctx) — wires schema to handler
def register(ctx):
    ctx.register_tool(
        name="hello_world",
        description="Respond to greetings",
        handler=hello_world_handler,
        schema={...}
    )
    ctx.register_hook("post_tool_call", log_every_call)

The four main APIs of PluginContext: register_tool (add a tool for the model to call), register_command (add a slash command shared by CLI + gateway), register_cli_command (add a hermes <plugin> ... subcommand), register_hook (run code at lifecycle points: before/after tool calls, before/after LLM calls, session start/end).

Opt-in safety by principle

This is the most important point to emphasize: a plugin does not run just because it is installed. Discovery finds it (it appears in hermes plugins list), but you must actively add it to plugins.enabled:

plugins:
  enabled:
    - hello-world
    - disk-cleanup
  disabled:          # deny-list — always wins on name conflict with enabled
    - noisy-plugin

When installing from GitHub, Hermes asks straight up “Enable ‘name’ now? [y/N]” — the default is no, forcing an explicit confirmation:

hermes plugins install owner/repo              # asks before enabling
hermes plugins install owner/repo --enable     # install + enable, for scripts
hermes plugins install owner/repo --ref <full-sha>  # pin to one exact commit, reproducible

Verify before trusting

hermes plugins doctor owner/repo --ci

This command re-runs the entire discovery pipeline, manifest parsing, and hook/tool registration exactly as real Hermes does — reporting wrong hook names, callbacks missing **kwargs, or mismatches between declaration and actual registration. Important note: this is not a sandbox — plugin code still runs in-process with the current user’s rights and can spawn subprocesses — so only run Doctor (and enable plugins) for code you trust enough.

Not every extension needs Python

The documentation emphasizes an easily missed point: some extension surfaces deliberately use shell commands via config instead of Python — TTS, STT, shell hooks. Any existing CLI “becomes a plugin” with just a YAML declaration, no code to write:

hooks:
  - event: post_tool_call
    command: "notify-send 'Tool: {tool_name} done'"
    when:
      tools: [terminal, patch, write_file]

Field-tested lessons

  • For simple needs (notifications, logging, calling a webhook when a tool finishes), always try a shell hook in config.yaml first — much faster than writing a full Python plugin.
  • When installing third-party plugins for production, always pin with --ref <full SHA> instead of branch/tag — to avoid the situation where plugin code changes silently after a git pull you were not aware of.
  • Run hermes plugins doctor --ci as a mandatory step in your review process before enabling any community plugin — do not just read the code with your eyes and trust it.