Skip to content
Back to skills

Hermes Desktop Plugins

ASecurity

hermes-desktop-plugins — Write desktop app plugins that add UI panes and commands.

  • 56 stars
  • 0 votes
  • 0 copies
  • 7 views
  • Added September 11, 2026
toolsjavascriptpythongojavareactapibackendsecurity

Works with

  • terminal
  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 11, 2026

npx -y skills add AtlasOmnia/hermes-custom-pack --skill hermes-desktop-plugins --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hermes Desktop Plugins?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hermes Desktop Plugins
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/atlasomnia-hermes-desktop-plugins/badge)](https://www.skillsdirectory.com/skills/atlasomnia-hermes-desktop-plugins)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: hermes-desktop-plugins
description: hermes-desktop-plugins — Write desktop app plugins that add UI panes and commands.
version: 1.0.0
platforms:
- linux
- macos
- windows
metadata:
 hermes:
 tags:
 - desktop
 - plugins
 - ui
 - extension
 category: productivity
 related_skills: []
---
# Hermes Desktop Plugins Skill

Write plugins for the Hermes desktop app: statusbar items, layout panes,
command-palette commands, keybinds, routes, and themes. A plugin is a single
plain-JavaScript ESM file the app loads at runtime — no build step, no repo
changes. A plugin can also talk to its own Python backend namespace
(`ctx.rest`/`ctx.socket` → `/api/plugins/<id>`); the general Python plugin
system (`~/.hermes/plugins/`) is otherwise documented separately.

Full human reference (every export, area payloads, backend, security):
`website/docs/developer-guide/desktop-plugin-sdk.md`.

## When to Use

- The user asks for a new desktop UI element (a pane, a statusbar widget, a
 dashboard, a command) without modifying the app itself.
- You want to surface data you compute (via gateway RPC) inside the app.

## Prerequisites

- The Hermes desktop app (it loads plugins; the CLI/gateway alone does not).
- Write access to `$HERMES_HOME/desktop-plugins/` (usually
 `~/.hermes/desktop-plugins/`).

## How to Run

1. Create `$HERMES_HOME/desktop-plugins/<name>/plugin.js` from
 `templates/plugin.js` (relative to this skill directory) — that's
 `~/.hermes/...` by default, or `~/.hermes/profiles/<profile>/...` under a
 named profile. Keep `<name>` equal to the plugin `id`.
2. The desktop app watches that directory: the plugin loads within a few
 seconds of the file landing, and every later save hot-reloads it in
 place. No reload step. (Fallback if it doesn't appear: ⌘K →
 **Reload desktop plugins**.)
3. If loading fails the app shows a toast naming the error — fix the file
 and save again.

## Quick Reference

The ONLY import surface is `@hermes/plugin-sdk` (plus `react` /
`react/jsx-runtime`, which resolve to the app's own React — write UI with
`jsx()` calls, not JSX syntax; the file is not compiled).

- `host.state.*` — readonly reactive atoms: `activeSessionId`, `cwd`,
 `gateway`, `model`, `profile`, `viewport`. Read with `.get()` in handlers,
 `useValue(atom)` in components.
- `host.request(method, params)` — gateway JSON-RPC (sessions, config,
 skills, cron — everything the app uses).
- `host.onEvent(type, fn)` — live gateway events (`'*'` for all). Returns a
 disposer.
- `host.notify({ kind, message })`, `host.navigate(path)`, `host.logs(...)`,
 `host.status()`, `haptic('tap')`.
- `ctx.register({ id, area, order?, render?, data? })` — contribute UI.
 Key areas: `'statusBar.right'`/`'statusBar.left'` (chips),
 `'panes'` (layout zones — set `title` and
 `data: { placement, dock?, width?, height? }`; the pane auto-joins a
 matching zone), `PALETTE_AREA` (⌘K commands), `KEYBINDS_AREA` (rebindable
 actions).
- Pane placement: `placement: 'left'|'right'|'bottom'|'main'` is the
 semantic role — the pane stacks (tabs) with existing panes of that role.
 To land on a specific EDGE instead, add `dock: { pane, pos }` — the same
 gesture as dragging onto a pane's drop chip. `pane` is any pane id
 (`workspace` is the main thread; also `sessions`, `terminal`, `files`,
 `review`, `logs`), `pos` is `'top'|'bottom'|'left'|'right'|'center'`.
 E.g. "below the conversation" = `dock: { pane: 'workspace', pos: 'bottom' }`
 — declare a `height` (e.g. `'200px'`) so it doesn't take half the zone.
- Full PAGES: register `area: ROUTES_AREA` with `data: { path: '/my-page' }`
 and a `render` — the page mounts in the workspace (main) pane like any
 built-in view. Make it reachable with a sidebar nav row:
 `ctx.register({ id: 'nav', area: SIDEBAR_NAV_AREA, data: { path: '/my-page', label: 'My Page', codicon: 'project' } })`
 (renders below Artifacts, lights up at the route) — and/or a
 `PALETTE_AREA` command calling `host.navigate('/my-page')`.
- `ctx.storage.get/set/remove` — persistence namespaced to your plugin.
- `ctx.i18n.register({ en, ja, ... })` — ship your OWN locale bundles, scoped
 to your plugin (never edit core `en.ts`). Values are literal strings or
 interpolator functions; nested trees are addressed by dot-path. Read them
 reactively in components with `usePluginI18n(id)` returning `t('key', ...args)`
 (re-renders on a locale switch), or via `ctx.i18n.t` in handlers/stores.
 Resolution follows the app's active locale, then your `en`, then the raw key.
- Data: `useQuery`/`useMutation`/`useQueryClient`/`queryClient` (the app's ONE
 React Query client — cache, dedupe, `refetchInterval`, invalidate like core;
 never hand-roll a poll loop), plus `atom`/`computed` for plugin-local state.
- Backend: if the plugin ships a Python `plugin_api.py` (under
 `~/.hermes/plugins/<id>/dashboard/`, manifest `"api": "plugin_api.py"`), reach
 it with `ctx.rest('/path', { method?, body?, timeoutMs? })` and its live twin
 `ctx.socket('/events', onMessage)` — both scoped to `/api/plugins/<id>` by
 construction (traversal rejected). `ctx.socket` is a **no-op on OAuth
 remotes**, so always keep a polling fallback. The Python backend is imported
 only when the plugin is in `plugins.enabled` in `config.yaml` (separate from
 the in-app enable toggle). For gateway-wide data use `host.request` /
 `host.onEvent` instead.
- `Contribute` (mount-scoped): render `jsx(Contribute, { area, id, children })`
 inside a component so page-owned chrome (e.g. a titlebar control in
 `TITLEBAR_AREAS.center`) leaves when the page unmounts — `ctx.register` is for
 permanent contributions.
- `defaultEnabled: false` on the default export ships an opt-in plugin: it
 inventories in Settings → Plugins, off until the user flips it on.
- Users manage plugins in Settings → Plugins (enable/disable live, reveal
 folder). A disabled plugin stays disabled across restarts — don't fight
 it; the user turned you off.
- UI: the app's design language, importable directly — `Button`, `Input`,
 `Textarea`, `Select*`, `Switch`, `Checkbox`, `SegmentedControl`, `Tabs*`,
 `Dialog*`, `ConfirmDialog`, `DropdownMenu*`, `ContextMenu*`, `Popover*`,
 `Tip`/`Tooltip*`, `Badge`, `Kbd`/`KbdGroup`, `SearchField`, `ScrollArea`,
 `Separator`, `Skeleton`, `GlyphSpinner`, `EmptyState`, `ErrorState`,
 `CopyButton`, `StatusDot`, `LogView`, `Codicon`, `DecodeText`, plus `cn`
 and `icons.*`. Prefer these over hand-rolled elements so the plugin looks
 native; style with theme vars, never hardcoded colors.

## Procedure

1. Pick a short kebab-case `id`; the folder name must match.
2. Start from `templates/plugin.js`; keep the default export shape
 (`{ id, name, register(ctx) }`).
3. For a pane, register `area: 'panes'` with a `placement` hint and a
 `render` returning your component — the app places it into a sensible
 zone automatically; the user can drag it anywhere afterwards.
4. Fetch data with `host.request` and/or subscribe with `host.onEvent`;
 never poll faster than a few seconds.
5. Write the file with your file tools, then ask the user to run
 **Reload desktop plugins** from ⌘K.

## Pitfalls

- NEVER hardcode colors or backgrounds (`#000`, `black`, `rgb(...)`). Panes
 already sit on the app's editor background — leave the background alone
 and use theme variables for everything else: `var(--ui-text-secondary)`,
 `var(--ui-text-quaternary)`, `var(--ui-stroke-secondary)`,
 `var(--ui-accent)`. For canvas drawing, resolve them once with
 `getComputedStyle(canvas).getPropertyValue('--ui-accent')`.
- Reference only what you imported — a component you forgot to import
 (e.g. `StatusDot`) is a ReferenceError at render. Double-check every
 identifier in your `jsx()` calls appears in the import line.
- Canvas panes MUST track their container with a `ResizeObserver` and
 re-size the canvas (width/height attributes, not just CSS) — panes resize
 constantly (sash drags, layout switches); a mount-time-only size leaves
 blank space or blurry scaling.
- JSX syntax will not parse — the file loads uncompiled. Use
 `jsx('div', { children: ... })` from `react/jsx-runtime`.
- Do not import anything except `@hermes/plugin-sdk`, `react`, and
 `react/jsx-runtime`; other specifiers fail to resolve.
- Handlers must read state imperatively (`$atom.get()`), never from render
 closures — rapid events will otherwise see stale values.
- Keep components small; subscribe (`useValue`) only in the leaf that
 renders the value.

## Verification

- The plugin's UI appears after **Reload desktop plugins**.
- No error toast ("Plugin <name> failed to load") appears; if it does, the
 message names the failure — fix and reload.
- For panes: the new zone is visible and draggable like any core pane.
## Public support files

- `templates/plugin.js`

Files in this skill

  • SKILL.md8.6 KB
  • templates/plugin.js3.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.