Learn Hermes Agent Advanced: Automation, Integrations & Production Event Hooks and the Plugin System

Event Hooks and the Plugin System

Advanced 🕐 24 min Lesson 14 of 16
What you'll learn
  • Distinguish gateway hooks, plugin hooks, and shell hooks and choose the right one for a given use case
  • Write a HOOK.yaml manifest and a handle(event_type, context) function for a gateway hook
  • Explain why plugin hooks can block tool calls and inject context while gateway hooks cannot
  • Configure a shell hook in config.yaml without writing any Python
  • Identify the four plugin types and which are single-select vs multi-select
  • Explain why plugins require explicit opt-in via plugins.enabled rather than auto-loading on discovery

Three Hook Systems, Not One

Hermes provides three complementary ways to react to things happening inside it, each suited to a different level of integration:

  • Gateway hooks -- fire during messaging platform operations (Telegram, Discord, Slack, and so on)
  • Plugin hooks -- run in both CLI and gateway contexts, registered via ctx.register_hook()
  • Shell hooks -- execute any shell script for blocking, formatting, or context injection, no Python required

All three are non-blocking by design: errors inside a hook are caught and logged rather than crashing the agent. A hook that fails should degrade gracefully, not take down whatever it was attached to.

Gateway Hooks: Structure

A gateway hook lives in its own directory:

~/.hermes/hooks/
└── my-hook/
    ├── HOOK.yaml
    └── handler.py

HOOK.yaml declares which events trigger it:

name: my-hook
description: Purpose of this hook
events:
  - event_type_1
  - event_type_2

handler.py defines a function literally named handle, which can be either synchronous or async:

async def handle(event_type: str, context: dict):
    """Process the event; can be async or regular def"""

Available Gateway Events

Event Triggers Context Contains
gateway:startup
Gateway process starts
platforms list
session:start
New messaging session
platform, user_id, session_id
session:end
Session closes
platform, user_id, session_key
agent:start
Agent begins processing
platform, user_id, message
agent:step
Each tool-loop iteration
platform, iteration, tool_names
agent:end
Agent finishes
platform, message, response
command:*
Any slash command (wildcard)
platform, command, args

A practical use of agent:step: alert yourself on Telegram if a single agent turn reaches an unusually high iteration count, which often signals it is stuck in a loop rather than making real progress. A practical use of command:*: log every slash command usage across platforms to a single file for later review.

Plugin Hooks: More Power, More Trust

Plugin hooks, registered inside a plugin's register() function, can do something gateway hooks cannot: block a tool call outright or inject additional context into what the LLM sees, rather than just observing after the fact.

def register(ctx):
    ctx.register_hook("pre_tool_call", my_function)
    ctx.register_hook("post_tool_call", another_function)

Key plugin hook points: pre_tool_call, post_tool_call, pre_llm_call, post_llm_call, session lifecycle events, and transform hooks for tool, terminal, or LLM output.

Shell Hooks: No Python Required

Configured directly in config.yaml:

hooks:
  pre_tool_call:
    - matcher: "terminal"
      command: "~/.hermes/agent-hooks/block-dangerous.sh"
      timeout: 5

The script receives JSON via stdin and returns JSON via stdout -- any executable with a proper shebang line works, regardless of language. This is the lowest-friction option if you already have a working script and do not want to wrap it in a Python plugin just to attach it.

The Plugin System Proper

Beyond hooks, plugins can add entirely new tools, slash commands, and even alternative backends. There are four plugin types:

  • General plugins -- tools, hooks, slash commands, CLI commands; multi-select, you can enable several at once
  • Memory providers -- replace the built-in memory system; single-select, only one active
  • Context engines -- replace context compression; single-select, only one active
  • Model providers -- declare new inference backends; multi-register, with per-session selection

Plugin Directory Structure

~/.hermes/plugins/my-plugin/
├── plugin.yaml      # manifest: name, version, description
├── __init__.py      # register() function
├── schemas.py        # tool schemas (LLM-visible definitions)
└── tools.py          # tool handlers (execution logic)

Hermes discovers plugins from several sources: ~/.hermes/plugins/ (user-level), .hermes/plugins/ (project-local), bundled plugins shipped inside the Hermes repo itself, and pip entry points for installed packages.

What register(ctx) Can Do

  • ctx.register_tool() -- add a new callable tool with its schema and handler
  • ctx.register_hook() -- attach a lifecycle callback, as shown above
  • ctx.register_command() -- add a new in-chat slash command
  • ctx.register_cli_command() -- add a new hermes <plugin> subcommand
  • ctx.register_skill() -- bundle skills under plugin:skill naming
  • ctx.inject_message() -- queue a message directly into an active conversation

Plugins Are Opt-In, Not Auto-Loaded

General plugins and any user-installed backend plugin are disabled by default. Discovery finds them, but nothing actually loads until you explicitly add it to plugins.enabled in config.yaml. hermes plugins gives you interactive toggling, and hermes plugins install prompts you for enablement at install time rather than silently activating anything. The one exception is bundled infrastructure -- platform adapters and default backends that ship with Hermes itself -- which loads automatically since it is core, trusted functionality rather than a third-party extension.

The next lesson covers the security model that governs everything you have built across this entire track, and how to run it safely once it is acting without your direct supervision.

Key takeaways
  • Plugin hooks can block tool calls and inject context into what the LLM sees -- gateway hooks can only observe and react after the fact, a meaningful capability difference between the two systems
  • Shell hooks require no Python -- any executable with a shebang that reads JSON from stdin and writes JSON to stdout works, the lowest-friction extension point in Hermes
  • Memory providers and context engines are single-select (only one active) while general plugins and model providers are multi-select -- the architecture matches what genuinely makes sense to run more than one of at a time
  • All hook systems are non-blocking by design -- errors are caught and logged, a failing hook should never crash the thing it is attached to
  • Plugins are disabled by default even after discovery -- explicit opt-in via plugins.enabled (or hermes plugins) is required before any third-party plugin code actually runs, though bundled core infrastructure is exempt from this