Skip to content

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.

Gnome mascot

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 app
  • host.state.* , reactive atoms for active session, cwd, gateway status, model, profile, viewport
  • host.request(method, params) , call the gateway JSON-RPC (sessions, config, skills, cron, everything the app uses)
  • host.onEvent(type, fn) , subscribe to live gateway events
  • host.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-status

Step 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

AreaWhat it addsExample
'panes'A new layout pane (dockable, draggable)System status, cron monitor, dashboard
'statusBar.right' / 'statusBar.left'A chip in the status barModel indicator, connection status
PALETTE_AREAA command in the Cmd+K palette"Check system health", "Run deployment"
KEYBINDS_AREAA rebindable keyboard shortcutCustom hotkeys for your plugin
ROUTES_AREAA full-page route (replaces the chat pane)Custom dashboard, settings page
SIDEBAR_NAV_AREAA nav item in the sidebarLink 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: ... }) from react/jsx-runtime.
  • Only import from @hermes/plugin-sdk, react, and react/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

  1. Create the plugin file at ~/.hermes/desktop-plugins/system-status/plugin.js
  2. Open the Desktop app (hermes desktop)
  3. The System Status pane should appear at the bottom of the workspace
  4. It should show the current gateway status, model, session ID, and working directory
  5. 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.