Dashboard Plugins
Dashboard plugins let you add custom tabs to the web dashboard. A plugin can display its own UI, call the Hermes API, and optionally register backend endpoints — all without touching the dashboard source code.
Quick Start
Create a plugin directory with a manifest and a JS file:
mkdir -p ~/.hermes/plugins/my-plugin/dashboard/dist
manifest.json:
{
"name": "my-plugin",
"label": "My Plugin",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills"
},
"entry": "dist/index.js"
}
dist/index.js:
(function () {
var SDK = window.__HERMES_PLUGIN_SDK__;
var React = SDK.React;
var Card = SDK.components.Card;
var CardHeader = SDK.components.CardHeader;
var CardTitle = SDK.components.CardTitle;
var CardContent = SDK.components.CardContent;
function MyPage() {
return React.createElement(Card, null,
React.createElement(CardHeader, null,
React.createElement(CardTitle, null, "My Plugin")
),
React.createElement(CardContent, null,
React.createElement("p", { className: "text-sm text-muted-foreground" },
"Hello from my custom dashboard tab!"
)
)
);
}
window.__HERMES_PLUGINS__.register("my-plugin", MyPage);
})();
Refresh the dashboard — your tab appears in the navigation bar.
Plugin Structure
Plugins live inside the standard ~/.hermes/plugins/ directory. The dashboard extension is a dashboard/ subfolder:
~/.hermes/plugins/my-plugin/
plugin.yaml # optional — existing CLI/gateway plugin manifest
__init__.py # optional — existing CLI/gateway hooks
dashboard/ # dashboard extension
manifest.json # required — tab config, icon, entry point
dist/
index.js # required — pre-built JS bundle
style.css # optional — custom CSS
plugin_api.py # optional — backend API routes
A single plugin can extend both the CLI/gateway (via plugin.yaml + __init__.py) and the dashboard (via dashboard/) from one directory.
Manifest Reference
The manifest.json file describes your plugin to the dashboard:
{
"name": "my-plugin",
"label": "My Plugin",
"description": "What this plugin does",
"icon": "Sparkles",
"version": "1.0.0",
"tab": {
"path": "/my-plugin",
"position": "after:skills"
},
"entry": "dist/index.js",
"css": "dist/style.css",
"api": "plugin_api.py"
}
| Field | Required | Description |
|---|---|---|
name | Yes | Unique plugin identifier (lowercase, hyphens ok) |
label | Yes | Display name shown in the nav tab |
description | No | Short description |
icon | No | Lucide icon name (default: Puzzle) |
version | No | Semver version string |
tab.path | Yes | URL path for the tab (e.g. /my-plugin) |
tab.position | No | Where to insert the tab: end (default), after:<tab>, before:<tab> |
entry | Yes | Path to the JS bundle relative to dashboard/ |
css | No | Path to a CSS file to inject |
api | No | Path to a Python file with FastAPI routes |
Tab Position
The position field controls where your tab appears in the navigation:
"end"— after all built-in tabs (default)"after:skills"— after the Skills tab"before:config"— before the Config tab"after:cron"— after the Cron tab
The value after the colon is the path segment of the target tab (without the leading slash).
Available Icons
Plugins can use any of these Lucide icon names:
Activity, BarChart3, Clock, Code, Database, Eye, FileText, Globe, Heart, KeyRound, MessageSquare, Package, Puzzle, Settings, Shield, Sparkles, Star, Terminal, Wrench, Zap
Unrecognized icon names fall back to Puzzle.
Plugin SDK
Plugins don't bundle React or UI components — they use the SDK exposed on window.__HERMES_PLUGIN_SDK__. This avoids version conflicts and keeps plugin bundles tiny.
SDK Contents
var SDK = window.__HERMES_PLUGIN_SDK__;
// React
SDK.React // React instance
SDK.hooks.useState // React hooks
SDK.hooks.useEffect
SDK.hooks.useCallback
SDK.hooks.useMemo
SDK.hooks.useRef
SDK.hooks.useContext
SDK.hooks.createContext
// API
SDK.api // Hermes API client (getStatus, getSessions, etc.)
SDK.fetchJSON // Raw fetch for custom endpoints — handles auth automatically
// UI Components (shadcn/ui style)
SDK.components.Card
SDK.components.CardHeader
SDK.components.CardTitle
SDK.components.CardContent
SDK.components.Badge
SDK.components.Button
SDK.components.Input
SDK.components.Label
SDK.components.Select
SDK.components.SelectOption
SDK.components.Separator
SDK.components.Tabs
SDK.components.TabsList
SDK.components.TabsTrigger
// Utilities
SDK.utils.cn // Tailwind class merger (clsx + twMerge)
SDK.utils.timeAgo // "5m ago" from unix timestamp
SDK.utils.isoTimeAgo // "5m ago" from ISO string
// Hooks
SDK.useI18n // i18n translations
SDK.useTheme // Current theme info
Using SDK.fetchJSON
For calling your plugin's backend API endpoints:
SDK.fetchJSON("/api/plugins/my-plugin/data")
.then(function (result) {
console.log(result);
})
.catch(function (err) {
console.error("API call failed:", err);
});
fetchJSON automatically injects the session auth token, handles errors, and parses JSON.
Using Existing API Methods
The SDK.api object has methods for all built-in Hermes endpoints:
// Fetch agent status
SDK.api.getStatus().then(function (status) {
console.log("Version:", status.version);
});
// List sessions
SDK.api.getSessions(10).then(function (resp) {
console.log("Sessions:", resp.sessions.length);
});
Backend API Routes
Plugins can register FastAPI routes by setting the api field in the manifest. Create a Python file that exports a router:
# plugin_api.py
from fastapi import APIRouter
router = APIRouter()
@router.get("/data")
async def get_data():
return {"items": ["one", "two", "three"]}
@router.post("/action")
async def do_action(body: dict):
return {"ok": True, "received": body}
Routes are mounted at /api/plugins/<name>/, so the above becomes:
GET /api/plugins/my-plugin/dataPOST /api/plugins/my-plugin/action
Plugin API routes bypass session token authentication since the dashboard server only binds to localhost.
Accessing Hermes Internals
Backend routes can import from the hermes-agent codebase:
from fastapi import APIRouter
from hermes_state import SessionDB
from hermes_cli.config import load_config
router = APIRouter()
@router.get("/session-count")
async def session_count():
db = SessionDB()
try:
count = len(db.list_sessions(limit=9999))
return {"count": count}
finally:
db.close()
Custom CSS
If your plugin needs custom styles, add a CSS file and reference it in the manifest:
{
"css": "dist/style.css"
}
The CSS file is injected as a <link> tag when the plugin loads. Use specific class names to avoid conflicts with the dashboard's existing styles.
/* dist/style.css */
.my-plugin-chart {
border: 1px solid var(--color-border);
background: var(--color-card);
padding: 1rem;
}
You can use the dashboard's CSS custom properties (e.g. --color-border, --color-foreground) to match the active theme.
Plugin Loading Flow
- Dashboard loads —
main.tsxexposes the SDK onwindow.__HERMES_PLUGIN_SDK__ App.tsxcallsusePlugins()which fetchesGET /api/dashboard/plugins- For each plugin: CSS
<link>injected (if declared), JS<script>loaded - Plugin JS calls
window.__HERMES_PLUGINS__.register(name, Component) - Dashboard adds the tab to navigation and mounts the component as a route
Plugins have up to 2 seconds to register after their script loads. If a plugin fails to load, the dashboard continues without it.
Plugin Discovery
The dashboard scans these directories for dashboard/manifest.json:
- User plugins:
~/.hermes/plugins/<name>/dashboard/manifest.json - Bundled plugins:
<repo>/plugins/<name>/dashboard/manifest.json - Project plugins:
./.hermes/plugins/<name>/dashboard/manifest.json(only whenHERMES_ENABLE_PROJECT_PLUGINSis set)
User plugins take precedence — if the same plugin name exists in multiple sources, the user version wins.
To force re-scanning after adding a new plugin without restarting the server:
curl http://127.0.0.1:9119/api/dashboard/plugins/rescan
Plugin API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/dashboard/plugins | GET | List discovered plugins |
/api/dashboard/plugins/rescan | GET | Force re-scan for new plugins |
/dashboard-plugins/<name>/<path> | GET | Serve plugin static assets |
/api/plugins/<name>/* | * | Plugin-registered API routes |
Example Plugin
The repository includes an example plugin at plugins/example-dashboard/ that demonstrates:
- Using SDK components (Card, Badge, Button)
- Calling a backend API route
- Registering via
window.__HERMES_PLUGINS__.register()
To try it, run hermes dashboard — the "Example" tab appears after Skills.
Tips
- No build step required — write plain JavaScript IIFEs. If you prefer JSX, use any bundler (esbuild, Vite, webpack) targeting IIFE output with React as an external.
- Keep bundles small — React and all UI components are provided by the SDK. Your bundle should only contain your plugin logic.
- Use theme variables — reference
var(--color-*)in CSS to automatically match whatever theme the user has selected. - Test locally — run
hermes dashboard --no-openand use browser dev tools to verify your plugin loads and registers correctly.