--- sidebar_label: "Catalog submission" title: "Submitting to the Plugin Catalog" description: "The full admission guidelines for the Hermes plugin catalog: what to check before you submit, the rules every entry follows, and what reviewers look at" --- # Submitting to the Plugin Catalog The [plugin catalog](../../user-guide/features/plugin-catalog.md) is a human-reviewed directory of Hermes plugins. Being listed is the trust signal: users install a catalog plugin by name, at the exact commit a maintainer read. This page holds the complete guidelines for getting a plugin in and keeping it there. The canonical copy of the admission rules is [`plugin-catalog/README.md`](https://github.com/NousResearch/hermes-agent/blob/main/plugin-catalog/README.md) in the repository. The [rules section](#admission-rules) below mirrors it word for word, and a test fails the build if the two drift apart. ## Before you submit - **A public repository.** The `repo` URL is an `https://` URL anyone can clone (GitHub or GitLab). - **A loadable plugin at the commit you pin.** The tree has a `plugin.yaml` manifest plus at least one entrypoint Hermes loads: `__init__.py` (Python), `desktop/plugin.js` (Desktop), `plugin.json` (portable Agent Plugin) or `dashboard/manifest.json` (web dashboard). If the plugin lives in a monorepo, point `subdir` at its directory. The [plugin developer guide](./index.md) covers the layout. - **Public surfaces only.** Extend Hermes through hooks, middleware, the `ctx.register_*` APIs, provider plugins and the [Desktop plugin SDK](../desktop-plugin-sdk.md). Never patch Hermes code or Desktop markup at runtime. If the hook you need is missing, see [Asking for a hook](#asking-for-a-hook). - **Validation passes locally.** Run the same check catalog CI runs, against a checkout at the commit you are about to pin: ```bash hermes plugins validate /path/to/your-plugin --install-deps ``` It checks the manifest and `requires_hermes`, that the plugin loads, that the `capabilities` you declare match what it registers, `config_schema` and `requires_env`, Python dependencies against Hermes's core constraints, the install security scan, and the `desktop surface` and `no core override` rules. Fix every failure before opening the PR, and read the warnings, since a reviewer will. ## Opening the PR 1. Add **one** file, `plugin-catalog/.yaml`, to [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent). The fields are documented in the README's [entry schema](https://github.com/NousResearch/hermes-agent/blob/main/plugin-catalog/README.md#entry-schema) and in [What's in an entry](../../user-guide/features/plugin-catalog.md#whats-in-an-entry). Pin `sha` to a full 40-character commit, and quote `version`. 2. In the PR description, say what the plugin does, which Hermes surfaces it uses, and everything rule 13 asks you to disclose. Add screenshots for anything with a UI. 3. Wait for the catalog CI job to go green. It clones your repo at the pinned commit and runs `hermes plugins validate`. 4. A maintainer reviews the pinned tree and merges, asks for changes, or declines. Rule 3 and rule 9 problems are declines rather than bug-fix requests: the plugin has to change its design before it can be listed. Your plugin's page at `/docs/plugins/` is built from the same file. The README at the pinned commit renders there by default, and `screenshots:` fills the gallery. There is no separate listing to maintain. ## Admission rules 1. **Human-merged gate.** Entries are added *only* via a PR to the `hermes-agent` repository, reviewed and merged by a maintainer. There is no self-serve registry, no automated ingestion. 2. **Exact SHA pins are mandatory.** Every entry pins a full 40-character commit SHA. Branches, tags, and short SHAs are rejected by the loader. Installs clone the repository and check out exactly that commit. 3. **No self-updating code.** A listed plugin must not fetch and replace its own files (in-app "check for updates", signed release downloaders, remote `plugin.js` loaders). The exact SHA pin *is* the trust model; a self-updater lets an installed copy move to a commit nobody reviewed. Updates reach users only through a SHA-bump PR here plus `hermes plugins update `. Keep the updater in the standalone distribution if you want one; strip it from the catalog build. 4. **SHA bumps are new PRs.** Updating an entry's pin is a new PR whose diff (old SHA → new SHA) is re-reviewed like any other change — reviewers are expected to look at the upstream commit range being adopted. 5. **Owner-or-major-contributor submissions, or a maintainer-curated sweep.** An entry may be submitted by the plugin repository's owner or a major contributor to it; drive-by submissions of third-party repos are declined. Hermes maintainers may also add entries in batches from a reviewed sweep of community plugins (every pin validated and scanned at the pinned commit, self-updater and credential-store checks run, English-first UI). Authors of swept-in entries keep control: a PR from the owner adjusting or removing their entry is accepted on request, and SHA bumps stay owner-or-maintainer PRs under rule 4. 6. **Declared capabilities must match reality.** The `capabilities:` block (tools, hooks, middleware, env vars) must match what the plugin actually registers at the pinned commit. Validation fails the entry otherwise — undeclared capability creep is treated as a security issue. 7. **The install scanner runs at admission.** `hermes plugins validate` includes the `security scan` check: `dangerous` fails the entry; `caution` findings appear as warnings in the CI log and the reviewer reads them before merging. In exchange, installs at the pinned SHA accept `caution` without a prompt (`dangerous` still blocks). Review the warnings; do not merge past them. 8. **Desktop plugins stay inside the SDK surface.** A `desktop/plugin.js` runs in the Desktop renderer with the app's full authority (the loader isolates errors, not capabilities), so a listed one may only use the plugin SDK: no prototype patching of renderer or browser objects (`Storage.prototype.y =`, `Object.defineProperty(...prototype`), which reviewers check by hand (the lint does not); a bundled library setting fields on classes it declares itself (three.js and the like) is fine, no `eval`/`new Function`, no `import()` of anything but `@hermes/plugin-sdk` / `react` (app bundle chunks, blob or http URLs included), no script-tag injection, no reaching into the app's internal stores or its own markup (querying `data-slot` / `data-tour` / `data-sidebar` / `data-testid` elements from `document`, or a `document.body` MutationObserver, to restyle, hide, click or rewrite core UI). `hermes plugins validate` refuses these at admission (`desktop surface` check) and warns, for the reviewer, on raw `window.hermesDesktop` bridge calls and on the app's markup selected any other way (`[data-slot]`, `[data-tree-*]` in CSS text or `closest()`); a plugin that needs a capability the SDK lacks asks for an SDK hook instead of patching around it. 9. **No runtime overrides of Hermes core.** A listed plugin extends Hermes only through public surfaces: hooks, middleware, provider profiles and the other `register_*` APIs, and Desktop SDK slots and routes. It must not replace, wrap or rebind core functions, methods, module attributes or private dicts in place (`AIAgent. = ...`, `setattr(server, ...)`, `sys.modules[...]`, writes into a core module's tables). Two plugins patching the same seam silently break each other, and every core release can break both. `hermes plugins validate` refuses these at admission (`no core override` check), and warns (without failing) on each import of an undocumented core module and each call to a private method of a core object it is handed (`db._execute_write(...)`), by file and line. If the hook you need does not exist, open an issue describing it: we would rather add the seam than list a patch. 10. **Dependency security policy is the plugin's.** Hermes's 3-day `exclude-newer` release-age check covers Hermes's own dependencies only, at the time Hermes locks them; a plugin's `python_dependencies` / `pyproject.toml` install under the plugin's policy (no age check), inside Hermes's core constraints: Hermes's exact pins cannot move. Reviewers read the dependency list at the pinned SHA: bare floors (`>=X` with no upper bound) and floors on the newest release get a request for the oldest API-compatible floor plus an upper bound, and authors are strongly recommended to run their own release quarantine (`uv --exclude-newer` in their CI) — see the developer guide's *Dependency security policy*. A recent floor alone is not grounds to hold an entry. 11. **Credentials come through Hermes, never from files.** A plugin gets secrets only through Hermes's routes. Its declared `requires_env` and `config_schema` secrets are read with `agent.secret_scope.get_secret(NAME)`, which honours the active profile (a bare `os.environ` read can pick up another profile's key under multiplexing). A subscription sign-in (Codex / ChatGPT today) is used only through `credentialed_provider_request` with `requires_auth`, so Hermes attaches the token and the plugin never holds it. An OAuth client for a third-party API (the user's own Google Cloud client, for example) is declared under `oauth:` in `plugin.yaml`, signed in with `hermes auth add :` and used through the same helper; Hermes stores and refreshes that token too. Its `authorize_url`, `token_url` and `hosts` are `https` only, and its `hosts` belong to the API the scopes are for. A plugin never reads `.env` files, Hermes's `auth.json`, another tool's login (a vendor CLI's token file, a browser profile, a keychain) or any other credential store directly, and never refreshes, rotates or writes OAuth tokens or presents itself as another vendor's client. If Hermes has no route for a credential a plugin needs, ask for one in a Hermes issue or PR; the plugin does not work around it. 12. **Approvals and unattended runs are respected.** A plugin never routes around Hermes's approval system: no auto-approving, no disabling guards, and no spawning Hermes or shell children that inherit YOLO or non-interactive mode to run commands nobody approved. Anything that waits for a person (a prompt, an OAuth browser flow) fails cleanly or times out under cron, the messaging gateway and other unattended runs instead of hanging the agent. 13. **Risky behaviour is disclosed.** What a user would want to know before installing goes in the PR description and the plugin's README: network calls to third-party services, reads outside the plugin's own data, shell commands, long-running background processes, stored credentials. Telemetry and usage reporting are opt-in. Reviewers summarise these as disclosure lines on the entry PR; undisclosed behaviour found in review is a request for changes. 14. **Compatibility metadata is truthful.** `requires_hermes` is a SemVer floor (`">=0.21.5"`), never a CalVer date, and never newer than the current release. The entry's `requires_hermes` is enforced: an install by catalog name refuses on a Hermes it rejects, and `hermes plugins update` keeps an installed plugin at its current pin until Hermes is updated. Declare the same floor in `plugin.yaml` as well, because older Hermes versions enforce only that one (the loader skips the plugin when it does not match). A portable package (`plugin.json`) has no `requires_hermes` field, so the catalog entry's `requires_hermes` is its only floor: set one when the package needs a recent Hermes (a server declaration key an older release ignores). `version` matches the pinned code, and Python dependencies resolve under Hermes's core constraints (`hermes plugins validate --install-deps` is what CI runs). 15. **No skins or forks of bundled plugins.** A change to a bundled plugin is a PR against `hermes-agent`, not a competing listing, and vendor-lookalike skins are not listed under Nous branding. 16. **One listing per plugin lineage.** A fork of a listed community plugin is listed only when it is materially different from the original: a different transport or architecture, or capability the original lacks and its author declined or has not answered a PR for 30 days. Improvements to a listed plugin go upstream as a PR to its author. A fork that renames, rebrands or adds small changes is declined in favour of the original. A listed fork names its origin in its disclosure line (`Derived from `). 17. **No unscannable executable payloads.** Everything a reviewer and the install scanner read at the pinned commit must be source they can read. A plugin must not ship a compiled binary (a native executable or library of any OS — detected by file header, not extension: ELF, Mach-O, PE/`MZ`, WebAssembly), committed Python bytecode (`.pyc`/`.pyo`, including sourceless or hash-forged caches that run instead of the `.py` beside them), or an archive whose members are not themselves reviewable source. A plain-text source bundle (a `.tar.gz` of `.py`/`.ts`/`.md` the plugin unpacks) is read like any other source; a bundle hiding a binary is not. The scanner cannot read machine code, and a checksum committed in the same commit only proves the file was not changed after that commit, not where it came from. If a plugin genuinely needs a native component, the binary must be built by the plugin repository's own public GitHub Actions from source in that repository and carry a GitHub build attestation (`actions/attest-build-provenance`) tying it to that workflow and commit; the gate verifies the attestation against the pinned commit (`gh attestation verify --repo --source-digest `). A binary a user's own machine fetches at setup time is allowed only from the upstream tool's official source, pinned by checksum, and disclosed (rule 13) — never bundled and never from an unofficial mirror. 18. **Password managers come from their vendor.** A plugin that registers a `browser_vault` login backend (`ctx.register_login_backend`) is listed only at `tier: verified`, submitted from the password-manager vendor's own repository by that vendor, or at `tier: official` when Nous maintains it. Community-tier, fork and third-party listings of a login backend are declined. Hermes enforces the same rule when the plugin loads: a login backend is accepted only from a plugin bundled with Hermes or installed from this catalog at `verified` or `official` tier and at the reviewed pin, as recorded in Hermes's own install record (never a file inside the plugin). Local-directory, Git URL, `--ref`, `--allow-removed`, project and pip installs are refused, and no setting relaxes it. ## Updating your entry Pin updates (bumping `sha` to a newer commit) go through the same PR and review process. A reviewer reads the commit range you are adopting. Bump `version` in the same PR so the label users see matches the code, and re-pin any `image` / `screenshots` URLs that embed the old sha. Installed plugins compare their recorded sha against the live pin: `hermes plugins list --json` reports `update_available`, the Desktop Plugins tab shows an **Update to 1.4.0** button, and `hermes plugins update ` checks out exactly the new pin. If a maintainer swept your plugin into the catalog and you want the entry changed or removed, open a PR on its file. Owners keep control of their entries. ## Delisting and removal - **Delisting** deletes an entry's file: for plugins that are unmaintained, superseded, squatting a name, or no longer meet a rule. Users who already installed the plugin keep it, and it is welcome back once the problem is fixed. When a new rule affects plugins that are already listed, their authors get an issue on their repository explaining the change and time to update before anything is removed. - **Removal** adds the plugin to [`removed.yaml`](https://github.com/NousResearch/hermes-agent/blob/main/plugin-catalog/README.md#removedyaml--the-blocklist), which is reserved for security and policy incidents. Installers refuse anything on that list, and installed copies stop updating and cannot be enabled. ## Asking for a hook If your plugin needs something the plugin surface doesn't offer, open an issue on [hermes-agent](https://github.com/NousResearch/hermes-agent/issues) describing what you need and why. We would much rather add a proper seam than list a patch, and a plugin that will use the hook is exactly the concrete consumer a new hook needs.