Skip to main content

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"
}
FieldRequiredDescription
nameYesUnique plugin identifier (lowercase, hyphens ok)
labelYesDisplay name shown in the nav tab
descriptionNoShort description
iconNoLucide icon name (default: Puzzle)
versionNoSemver version string
tab.pathYesURL path for the tab (e.g. /my-plugin)
tab.positionNoWhere to insert the tab: end (default), after:<tab>, before:<tab>
entryYesPath to the JS bundle relative to dashboard/
cssNoPath to a CSS file to inject
apiNoPath 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/data
  • POST /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​

  1. Dashboard loads — main.tsx exposes the SDK on window.__HERMES_PLUGIN_SDK__
  2. App.tsx calls usePlugins() which fetches GET /api/dashboard/plugins
  3. For each plugin: CSS <link> injected (if declared), JS <script> loaded
  4. Plugin JS calls window.__HERMES_PLUGINS__.register(name, Component)
  5. 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:

  1. User plugins: ~/.hermes/plugins/<name>/dashboard/manifest.json
  2. Bundled plugins: <repo>/plugins/<name>/dashboard/manifest.json
  3. Project plugins: ./.hermes/plugins/<name>/dashboard/manifest.json (only when HERMES_ENABLE_PROJECT_PLUGINS is 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​

EndpointMethodDescription
/api/dashboard/pluginsGETList discovered plugins
/api/dashboard/plugins/rescanGETForce re-scan for new plugins
/dashboard-plugins/<name>/<path>GETServe 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-open and use browser dev tools to verify your plugin loads and registers correctly.