2cc23a9 -> c4123fbdb315137b40e4c5b4b5fd3eb2298dae66. The pinned commit runs its suite on ubuntu-latest, macos-latest and windows-latest (actions/runs/35520540211, all green), so windows is declared in platforms instead of being excluded without explanation, and the README now gives the manual install route that Windows needs because install.sh is a POSIX shell script. What CI found and fixed on the way to that green run, all of it reaching the plugin only through this branch: an in-place overwrite of a multi-frame image failed on Windows because the input's read handle (and the mapping Pillow uses for single-strip BMP/TIFF/PNG/P) was still open at the rename; a read-only target is refused by Windows even with the mode restored afterwards, so the attribute is cleared for the rename and put back either way; WinError 5/32 from an external holder is retried briefly; release/rename/chmod hold the plugin's guard lock; and converting a JPEG with EXIF orientation 5-8 to TIFF no longer fails its own verification (Pillow's TIFF reader applies the tag, so the file legitimately reads back transposed). Diff adopted: 5 commits, tools.py +158/-23, tests +366, README +5, 135 tests (134 + 1 platform skip on Windows).
Hermes Plugin Catalog
Curated, Nous-approved Hermes plugins. Each YAML file in this directory
(except removed.yaml) is one catalog entry, discoverable via
hermes plugins catalog / hermes plugins search and installable with
hermes plugins install <name>.
Admission policy
Presence in this directory is the trust signal. The rules that keep it meaningful:
- Human-merged gate. Entries are added only via a PR to the
hermes-agentrepository, reviewed and merged by a maintainer. There is no self-serve registry, no automated ingestion. - 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.
- 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.jsloaders). 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 plushermes plugins update <name>. Keep the updater in the standalone distribution if you want one; strip it from the catalog build. - 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.
- 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.
- 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. - The install scanner runs at admission.
hermes plugins validateincludes thesecurity scancheck:dangerousfails the entry;cautionfindings appear as warnings in the CI log and the reviewer reads them before merging. In exchange, installs at the pinned SHA acceptcautionwithout a prompt (dangerousstill blocks). Review the warnings; do not merge past them. - Desktop plugins stay inside the SDK surface. A
desktop/plugin.jsruns 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 (X.prototype.y =,Object.defineProperty(...prototype), noeval/new Function, noimport()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.hermes plugins validaterefuses these at admission (desktop surfacecheck); a plugin that needs a capability the SDK lacks asks for an SDK hook instead of patching around it.
Entry schema
name: example-plugin # [a-z0-9_-]{1,64}, the catalog key
repo: https://github.com/owner/repo # https:// only
sha: <40-hex commit sha> # mandatory exact pin
subdir: "" # optional path within the repo
description: One-line description.
maintainer: OwnerName
tier: official # official | community (default community)
category: memory # desktop | memory | platform | web | tools | voice | automation | models | general
# (default desktop) — the shelf the entry sits on at /docs/plugins
requires_hermes: ">=0.19" # optional
docs_url: "" # optional
version: "1.4.0" # optional human label for the sha (quote it); shown as "1.4.0 @ abcd1234"
image: "" # optional https image on a GitHub host, 2:1 (e.g. 1200x600), e.g.
# https://raw.githubusercontent.com/owner/repo/<sha>/docs/banner.png
platforms: [] # optional, e.g. [linux, macos]; empty = all
capabilities:
provides_tools: []
provides_hooks: []
provides_middleware: []
requires_env: []
version and image are cosmetic: neither is parsed or used to pick what
installs. The sha stays the release; bump version in the same PR that bumps
sha so the label on the card matches the code. Images must live on
raw.githubusercontent.com, github.com or *.githubusercontent.com so
the Desktop catalog never fetches from third-party hosts; pin the raw URL to
the entry's commit and the picture is as immutable as the code.
removed.yaml — the blocklist
When an entry is pulled from the catalog for security or policy reasons, it
is recorded in removed.yaml with a reason and date. The installer refuses
to install anything matching a removed entry's name or repo URL, so a
malicious plugin cannot be re-installed from a stale identifier after
removal. Removals, like additions, land via reviewed PRs.
Delisting is different from removal: an entry that is merely unmaintained, superseded, or squatting a name it is not affiliated with is deleted from the catalog (plain file removal, users who already installed it are unaffected) and is welcome back under a distinct name.
Names
The catalog key and the manifest name: are what users search, install, and — for memory
providers — put in memory.provider. A memory / exclusive entry must not reuse the name of
another provider or of a well-known upstream project it is not affiliated with: two providers
registering the same register_memory_provider name make memory.provider ambiguous
(whichever loads last wins). Reviewers check the registered provider name, not just the file
name, and the affiliated project gets the bare key.