Hermes Agent Tutorial Part 5: Advanced Configuration & Custom Extensions

By this point in the series, you should be comfortable with Hermes Agent’s core workflow: running tasks, managing files, and leveraging built-in tools. In Part 5, we dive deep into the engine room. We will explore how to bend Hermes Agent to your will through custom model providers, visual skins, plugins, multi-platform gateways, and environment-aware profiles.

These features transform Hermes Agent from a convenient assistant into a tailored powerhouse that matches your infrastructure, brand, and operational requirements.


1. Custom Model Providers

Hermes Agent ships with support for several providers out of the box, but the real power lies in its provider abstraction layer. You can register any OpenAI-compatible API—or even build a custom adapter for proprietary endpoints.

1.1 Registering OpenRouter

OpenRouter provides a unified interface to hundreds of models. Adding it to Hermes Agent takes only a few lines in your configuration.

Create or edit ~/.config/hermes-agent/providers.yaml:

providers:
  openrouter:
    base_url: "https://openrouter.ai/api/v1"
    api_key: "${OPENROUTER_API_KEY}"
    default_model: "anthropic/claude-3.5-sonnet"
    timeout: 120
    headers:
      HTTP-Referer: "https://yourdomain.com"
      X-Title: "Hermes Agent Production"
    models:
      - id: "anthropic/claude-3.5-sonnet"
        context_window: 200000
        max_tokens: 8192
      - id: "openai/gpt-4o"
        context_window: 128000
        max_tokens: 4096
      - id: "google/gemini-1.5-pro"
        context_window: 1000000
        max_tokens: 8192

Then set your API key via environment variable (never hardcode secrets):

export OPENROUTER_API_KEY="sk-or-v1-xxxxxxxxxxxxxxxx"

Switch to the new provider at runtime:

hermes-agent --provider openrouter --model "anthropic/claude-3.5-sonnet"

1.2 Building a Custom Provider Adapter

If your organization runs an internal model gateway, you can write a lightweight Python adapter. Save this as ~/.config/hermes-agent/plugins/my_provider.py:

from hermes_agent.providers import BaseProvider, ChatMessage
import requests

class AcmeCorpProvider(BaseProvider):
    name = "acme-corp"

    def __init__(self, config):
        self.base_url = config["base_url"]
        self.api_key = config["api_key"]
        self.model = config.get("default_model", "acme-llm-large")

    def chat(self, messages: list[ChatMessage], **kwargs):
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
        payload = {
            "model": kwargs.get("model", self.model),
            "messages": [{"role": m.role, "content": m.content} for m in messages],
            "temperature": kwargs.get("temperature", 0.7),
            "max_tokens": kwargs.get("max_tokens", 4096)
        }
        resp = requests.post(
            f"{self.base_url}/v1/chat/completions",
            headers=headers,
            json=payload,
            timeout=kwargs.get("timeout", 60)
        )
        resp.raise_for_status()
        data = resp.json()
        return data["choices"][0]["message"]["content"]

    def list_models(self):
        return ["acme-llm-large", "acme-llm-small", "acme-code-specialist"]

Register it in ~/.config/hermes-agent/config.yaml:

plugins:
  model_providers:
    - path: "plugins/my_provider.py"
      class: "AcmeCorpProvider"

providers:
  acme-corp:
    base_url: "https://llm.internal.acme.com"
    api_key: "${ACME_LLM_API_KEY}"
    default_model: "acme-llm-large"

Hermes Agent dynamically loads the class at startup, making your internal models available alongside commercial ones.


2. Skin Engine: Customizing the CLI Appearance

The Skin Engine lets you redefine how Hermes Agent renders output in the terminal. This is invaluable when you want color schemes that match your IDE, compact layouts for small screens, or branded styling for demos.

2.1 Anatomy of a Skin

Skins are YAML files stored in ~/.config/hermes-agent/skins/. A minimal skin looks like this:

# ~/.config/hermes-agent/skins/minimal.yaml
name: "Minimal"
author: "Your Name"
version: "1.0.0"

palette:
  primary: "#61afef"
  success: "#98c379"
  warning: "#e5c07b"
  error: "#e06c75"
  muted: "#5c6370"
  background: "#282c34"
  text: "#abb2bf"

layout:
  compact: true
  show_timestamps: false
  max_line_width: 100
  indent_size: 2

components:
  prompt:
    prefix: "› "
    color: "primary"
  tool_call:
    badge: "[TOOL]"
    color: "warning"
  code_block:
    theme: "monokai"
    show_line_numbers: true
  progress:
    style: "dots"   # options: bar, dots, spinner
    color: "primary"

Activate it:

hermes-agent --skin minimal
# or persist it
hermes-agent config set skin minimal

2.2 Advanced Skin: Branded Enterprise Theme

For teams that want consistent branding across CLI tools:

# ~/.config/hermes-agent/skins/enterprise.yaml
name: "Enterprise"
author: "Acme Corp DevOps"
version: "2.1.0"

palette:
  primary: "#0052cc"
  secondary: "#0747a6"
  success: "#36b37e"
  warning: "#ffab00"
  error: "#de350b"
  info: "#00b8d9"
  muted: "#6b778c"
  background: "#f4f5f7"
  text: "#172b4d"
  border: "#dfe1e6"

layout:
  compact: false
  show_timestamps: true
  timestamp_format: "%H:%M:%S"
  max_line_width: 120
  indent_size: 4
  panel_borders: true

components:
  header:
    show_logo: true
    logo_text: "ACME AI"
    logo_color: "primary"
    separator: "═"
  prompt:
    prefix: "λ "
    color: "secondary"
    bold: true
  tool_call:
    badge: "⚙ EXEC"
    color: "info"
    show_duration: true
  file_diff:
    added_line_color: "success"
    removed_line_color: "error"
    context_line_color: "muted"
  progress:
    style: "bar"
    color: "primary"
    width: 40

The Skin Engine supports conditional rules too. For example, automatically switch to compact mode when the terminal width drops below 80 columns:

rules:
  - condition: "terminal.width < 80"
    overrides:
      layout.compact: true
      layout.show_timestamps: false
      components.header.show_logo: false

3. Plugin System

Hermes Agent’s plugin architecture is built around three extension points: memory, context_engine, and model-providers. We already covered model providers; now let’s explore the other two.

3.1 Memory Plugins

By default, Hermes Agent retains conversation context for the current session only. A memory plugin lets you persist context across sessions, share memory between agents, or integrate with external vector databases.

Here is a SQLite-backed memory plugin:

# ~/.config/hermes-agent/plugins/sqlite_memory.py
import sqlite3
import json
from datetime import datetime
from hermes_agent.plugins import MemoryPlugin

class SQLiteMemory(MemoryPlugin):
    name = "sqlite-memory"

    def __init__(self, config):
        self.db_path = config.get("db_path", "~/.local/share/hermes-agent/memory.db")
        self._init_db()

    def _init_db(self):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS memories (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    session_id TEXT,
                    role TEXT,
                    content TEXT,
                    metadata TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            """)
            conn.execute("""
                CREATE INDEX IF NOT EXISTS idx_session ON memories(session_id)
            """)

    def store(self, session_id: str, role: str, content: str, metadata: dict = None):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                "INSERT INTO memories (session_id, role, content, metadata) VALUES (?, ?, ?, ?)",
                (session_id, role, content, json.dumps(metadata or {}))
            )

    def retrieve(self, session_id: str, limit: int = 50):
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                "SELECT role, content, metadata FROM memories WHERE session_id = ? ORDER BY created_at DESC LIMIT ?",
                (session_id, limit)
            )
            rows = cursor.fetchall()
        return [
            {"role": r, "content": c, "metadata": json.loads(m)}
            for r, c, m in reversed(rows)
        ]

    def search(self, query: str, top_k: int = 5):
        # Simple keyword search; swap in vector search for production
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                "SELECT session_id, content FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?",
                (f"%{query}%", top_k)
            )
            return [{"session_id": s, "content": c} for s, c in cursor.fetchall()]

Register it:

plugins:
  memory:
    driver: "sqlite-memory"
    config:
      db_path: "~/.local/share/hermes-agent/memory.db"

3.2 Context Engine Plugins

Context engines determine what information is injected into the model’s prompt beyond the immediate conversation. A custom context engine can pull live metrics, documentation, or ticket data.

# ~/.config/hermes-agent/plugins/jira_context.py
import requests
from hermes_agent.plugins import ContextEnginePlugin

class JiraContextEngine(ContextEnginePlugin):
    name = "jira-context"

    def __init__(self, config):
        self.base_url = config["base_url"]
        self.email = config["email"]
        self.api_token = config["api_token"]
        self.auth = (self.email, self.api_token)

    def gather(self, query: str) -> str:
        """Fetch relevant Jira tickets and format them as context."""
        jql = f'text ~ "{query}" ORDER BY updated DESC'
        resp = requests.get(
            f"{self.base_url}/rest/api/2/search",
            params={"jql": jql, "maxResults": 5, "fields": "summary,status,description"},
            auth=self.auth,
            timeout=10
        )
        resp.raise_for_status()
        issues = resp.json().get("issues", [])
        if not issues:
            return ""
        lines = ["## Relevant Jira Tickets"]
        for issue in issues:
            fields = issue["fields"]
            lines.append(f"- **{issue['key']}**: {fields['summary']} ({fields['status']['name']})")
        return "\n".join(lines)

Enable it:

plugins:
  context_engines:
    - name: "jira-context"
      priority: 10
      config:
        base_url: "https://acme.atlassian.net"
        email: "${JIRA_EMAIL}"
        api_token: "${JIRA_API_TOKEN}"

Now every prompt automatically includes relevant Jira context when keywords match.


4. Gateway Configuration: Multi-Platform Message Routing

The Gateway module connects Hermes Agent to external messaging platforms—Slack, Discord, Feishu (Lark), Microsoft Teams, and generic webhooks. This turns Hermes Agent into a bot that can receive requests and post responses across your organization.

4.1 Feishu (Lark) Gateway

Feishu is the platform you are using right now. Here is a complete gateway configuration:

# ~/.config/hermes-agent/gateway.yaml
gateways:
  feishu:
    enabled: true
    app_id: "${FEISHU_APP_ID}"
    app_secret: "${FEISHU_APP_SECRET}"
    encrypt_key: "${FEISHU_ENCRYPT_KEY}"   # optional, for event encryption
    verification_token: "${FEISHU_VERIFICATION_TOKEN}"
    event_types:
      - "im.message.receive_v1"
    bot_config:
      mention_required: true       # only respond when @mentioned
      reply_in_thread: true
      max_message_length: 4000
      allowed_chat_types:
        - "p2p"
        - "group"
    webhook:
      bind_host: "0.0.0.0"
      bind_port: 8080
      path: "/webhook/feishu"
      tls:
        enabled: false
        # cert_file: "/etc/hermes-agent/cert.pem"
        # key_file: "/etc/hermes-agent/key.pem"

Start the gateway:

hermes-agent gateway start --config ~/.config/hermes-agent/gateway.yaml

For production, place the gateway behind a reverse proxy (Nginx, Caddy, or cloud load balancer) and configure the Feishu developer portal to point to your public URL.

4.2 Multi-Gateway Routing Rules

You can run multiple gateways simultaneously and route messages based on rules:

gateways:
  slack:
    enabled: true
    bot_token: "${SLACK_BOT_TOKEN}"
    signing_secret: "${SLACK_SIGNING_SECRET}"
    webhook:
      bind_port: 8081
      path: "/webhook/slack"

  discord:
    enabled: true
    bot_token: "${DISCORD_BOT_TOKEN}"
    gateway_intents: ["GUILD_MESSAGES", "DIRECT_MESSAGES"]

routing:
  default_gateway: "feishu"
  rules:
    - match:
        source: "slack"
        channel: "#devops-alerts"
      action:
        profile: "production"
        model: "openai/gpt-4o"
    - match:
        source: "feishu"
        chat_type: "group"
      action:
        profile: "team"
        require_mention: true

This ensures that alerts from Slack #devops-alerts run with a high-capacity model and production profile, while Feishu group chats use the team profile and require an explicit mention.


5. Profile System: Managing Multi-Environment Configurations

Profiles let you switch between entire configuration sets with a single flag. This is essential when you operate Hermes Agent across development, staging, and production environments.

5.1 Defining Profiles

Profiles live in ~/.config/hermes-agent/profiles/:

# ~/.config/hermes-agent/profiles/development.yaml
name: "development"
description: "Local dev setup with fast, cheap models"

provider:
  name: "openrouter"
  model: "openai/gpt-4o-mini"
  temperature: 0.9
  max_tokens: 2048

plugins:
  memory:
    driver: "sqlite-memory"
    config:
      db_path: "./dev_memory.db"

skin: "minimal"

gateway:
  enabled: false

logging:
  level: "debug"
  file: "./hermes-dev.log"
# ~/.config/hermes-agent/profiles/production.yaml
name: "production"
description: "Production deployment with full audit trail"

provider:
  name: "openrouter"
  model: "anthropic/claude-3.5-sonnet"
  temperature: 0.2
  max_tokens: 8192

plugins:
  memory:
    driver: "sqlite-memory"
    config:
      db_path: "/var/lib/hermes-agent/memory.db"
  context_engines:
    - name: "jira-context"
      priority: 10
      config:
        base_url: "https://acme.atlassian.net"
        email: "${JIRA_EMAIL}"
        api_token: "${JIRA_API_TOKEN}"

skin: "enterprise"

gateway:
  enabled: true
  config_file: "/etc/hermes-agent/gateway.yaml"

logging:
  level: "info"
  file: "/var/log/hermes-agent/agent.log"
  format: "json"

audit:
  enabled: true
  retention_days: 90
  destination: "/var/log/hermes-agent/audit/"

5.2 Switching Profiles

Activate a profile at startup:

hermes-agent --profile production

Or set the default:

hermes-agent config set default_profile production

You can also override individual values without editing the profile file:

hermes-agent --profile production --provider.model "openai/gpt-4o" --logging.level debug

5.3 Environment Variable Injection

Profiles support environment variable substitution with ${VAR} syntax. For sensitive values, use a .env file loaded automatically from the profile directory:

# ~/.config/hermes-agent/profiles/production.env
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxx
JIRA_EMAIL=[email protected]
JIRA_API_TOKEN=xxxxxxxx
FEISHU_APP_ID=cli_xxxxxxxx
FEISHU_APP_SECRET=xxxxxxxx

Hermes Agent loads these variables before parsing the profile YAML, keeping secrets out of version control.


6. Putting It All Together: A Complete Custom Setup

Here is a consolidated example that ties every concept from this tutorial into one working configuration tree:

~/.config/hermes-agent/
├── config.yaml
├── providers.yaml
├── gateway.yaml
├── profiles/
│   ├── development.yaml
│   ├── development.env
│   ├── production.yaml
│   └── production.env
├── skins/
│   ├── minimal.yaml
│   └── enterprise.yaml
└── plugins/
    ├── my_provider.py
    ├── sqlite_memory.py
    └── jira_context.py

config.yaml (root configuration):

default_profile: "development"
config_dirs:
  skins: "./skins"
  plugins: "./plugins"
  profiles: "./profiles"

Start in development:

hermes-agent --profile development

Promote to production with identical commands but stricter guardrails:

hermes-agent --profile production gateway start

Summary

In this tutorial, you learned how to:

These advanced features ensure that Hermes Agent scales from a personal productivity tool to an enterprise-grade automation platform. In Part 6, we will cover automation recipes, CI/CD integration, and headless deployment patterns.

Stay tuned, and happy building!