Appearance
Desktop Plugins
The Hermes Desktop app can be extended with JavaScript plugins that add panes, statusbar widgets, command palette entries, keybindings, and full-page routes. A plugin is a single ESM file, no build step, no repo changes. The app watches your plugin directory and hot-reloads on save. This lesson covers the SDK and builds a working plugin.

What you'll learn
- Plugins are plain JavaScript ESM files under
~/.hermes/desktop-plugins/<name>/plugin.js - The SDK provides reactive state atoms, gateway JSON-RPC, event subscriptions, and the app's UI component library
- Panes can dock to any edge of the workspace, sidebar, or other panes, the user can drag them anywhere afterward
The problem
The Desktop app has a session list, a chat pane, and a file explorer. That covers most workflows. But if you want a live system status panel, a cron job monitor, or a dashboard that pulls data from your own API, you need to extend the app. The plugin SDK lets you do that without touching the app's source code.
How it works
A plugin is a single file at ~/.hermes/desktop-plugins/<id>/plugin.js that exports { id, name, register(ctx) }. The register function receives a context object with everything you need:
ctx.register({ id, area, order?, render?, data? }), contribute UI to different areas of the apphost.state.*, reactive atoms for active session, cwd, gateway status, model, profile, viewporthost.request(method, params), call the gateway JSON-RPC (sessions, config, skills, cron, everything the app uses)host.onEvent(type, fn), subscribe to live gateway eventshost.notify(),host.navigate(),host.logs(),host.status()
The app's design system is importable directly , Button, Input, Dialog, Badge, ScrollArea, StatusDot, and more. Use these instead of hand-rolled elements so the plugin looks native.
Build it: a system status panel
Step 1: Create the plugin directory
Set up the standard folder structure for your new desktop extension. The app monitors this path automatically for changes.
bash
mkdir -p ~/.hermes/desktop-plugins/system-statusStep 2: Write the plugin
Drop this code into your main file to define the system status panel layout and reactive data binding.
javascript
// ~/.hermes/desktop-plugins/system-status/plugin.js
import { jsx } from 'react/jsx-runtime';
import { useValue } from '@hermes/plugin-sdk';
import { StatusDot, Badge, Button } from '@hermes/plugin-sdk';
export default {
id: 'system-status',
name: 'System Status',
register(ctx) {
const { host } = ctx;
ctx.register({
id: 'system-status-pane',
area: 'panes',
data: {
title: 'System Status',
placement: 'right',
dock: { pane: 'workspace', pos: 'bottom' },
height: '200px',
},
render: () => {
const activeSession = useValue(host.state.activeSessionId);
const gateway = useValue(host.state.gateway);
const cwd = useValue(host.state.cwd);
const model = useValue(host.state.model);
return jsx('div', {
style: {
padding: '12px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
fontSize: '13px',
},
children: [
jsx('div', {
style: { display: 'flex', alignItems: 'center', gap: '8px' },
children: [
jsx(StatusDot, { status: gateway ? 'online' : 'offline' }),
jsx('span', { children: `Gateway: ${gateway ? 'Connected' : 'Offline'}` }),
],
}),
jsx(Badge, { children: `Model: ${model || 'None'}` }),
jsx('div', { children: `Session: ${activeSession || 'None'}` }),
jsx('div', { children: `CWD: ${cwd || 'Unknown'}` }),
jsx(Button, {
onClick: () => host.request('hermes.health', {}),
children: 'Check Health',
}),
],
});
},
});
},
};Step 3: Load the plugin
The Desktop app watches ~/.hermes/desktop-plugins/ automatically. The plugin loads within a few seconds of the file landing. If it doesn't appear, open the command palette (Cmd+K/Ctrl+K) and run Reload desktop plugins.
If loading fails, the app shows a toast naming the error, fix the file and save again. No restart needed.
Key areas you can register
| Area | What it adds | Example |
|---|---|---|
'panes' | A new layout pane (dockable, draggable) | System status, cron monitor, dashboard |
'statusBar.right' / 'statusBar.left' | A chip in the status bar | Model indicator, connection status |
PALETTE_AREA | A command in the Cmd+K palette | "Check system health", "Run deployment" |
KEYBINDS_AREA | A rebindable keyboard shortcut | Custom hotkeys for your plugin |
ROUTES_AREA | A full-page route (replaces the chat pane) | Custom dashboard, settings page |
SIDEBAR_NAV_AREA | A nav item in the sidebar | Link to your custom page |
Pitfalls
- Never hardcode colors. Use theme variables:
var(--ui-text-secondary),var(--ui-accent),var(--ui-stroke-secondary). Panes already sit on the app's background, leave it alone. - JSX syntax won't parse. The file loads uncompiled. Use
jsx('div', { children: ... })fromreact/jsx-runtime. - Only import from
@hermes/plugin-sdk,react, andreact/jsx-runtime. Other specifiers fail to resolve. - Handlers must read state imperatively (
atom.get()), not from render closures, rapid events will otherwise see stale values. - Canvas panes must track their container with a
ResizeObserver, panes resize constantly as the user drags sashes.
Confirm it worked
- Create the plugin file at
~/.hermes/desktop-plugins/system-status/plugin.js - Open the Desktop app (
hermes desktop) - The System Status pane should appear at the bottom of the workspace
- It should show the current gateway status, model, session ID, and working directory
- Change the model in the status bar, the pane should update reactively
Next: Custom Tools & Python Plugins , registering your own tools in Hermes's tool registry.