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:
- Register custom model providers like OpenRouter and internal APIs.
- Build a custom provider adapter in Python for proprietary endpoints.
- Design CLI skins that match your workflow or corporate branding.
- Develop memory and context plugins to persist state and inject live data.
- Configure multi-platform gateways including Feishu, Slack, and Discord.
- Manage environment profiles with isolated settings and secret injection.
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!