Contributing
August 21, 2026 · View on GitHub
The registry is the JSON, not the README. All contributions are PRs against
data/plugins.json; the README is regenerated from it.
Submit a plugin
-
Add one entry to the
pluginsarray indata/plugins.json. Field reference lives indata/schema.json; copy an existing entry as a starting point. -
Regenerate and check:
node scripts/validate.mjs node scripts/render.mjs -
Commit both files and open the PR. CI runs the same two checks.
Set verifiedAgainst to the dsh version you actually tested, lastVerified
to today, and status honestly. unverified entries are accepted; lying in
verified gets the entry pulled.
The spam gate
Every entry, whether submitted by PR or promoted from the discovery queue, must clear this bar:
- Real install path. One of:
- a
dsh.bundlemanifest inpackage.json(bundle or plugin), - a published npm package a profile can depend on,
- a
SKILL.mdlayout dsh actually discovers (top-level<name>/SKILL.mdor flat<name>.md; dsh does no nested discovery), whose frontmatter dsh will accept: a---line first, a closing---, and both a kebab-casenameand adescription. Miss any of those and dsh logsskill file <path> ignoredand moves on, so the file is markdown, not a skill (packages/skill/skill-filesystem/src/index.ts).
- a
- Not a fork of a template with the name swapped. History and substance are checked, not just the GitHub fork flag; generated template spam counts too.
- Loads against the dsh version claimed in
verifiedAgainst, for exampledsh --profile <p> --dump-configsucceeds with the package installed, or the skill appears in the skill list. - Honest description, no keyword stuffing. Riding the
dsh-plugintopic without extending dsh is exactly what this registry filters out.
The discovery queue
A scheduled workflow (.github/workflows/watch.yml)
sweeps every dsh discovery topic (dsh-plugin, dsh-plugins, dsh-theme,
dsh-skill, dsh-bundle), npm, and GitHub code search, and queues new finds
in data/candidates.json on a single reused triage PR.
Nothing is promoted automatically.
The reading is automated; the admitting is not. On every sweep,
scripts/triage.mjs opens each queued repo's own files —
package.json, SKILL.md, and if the root says nothing, the whole tree — and
reports what it found in the PR body. A maintainer applies it on a branch:
npm run triage:dry # decide everything, write nothing
npm run triage # apply: admit with evidence, reject with a reason
npm run render && npm run validate
Admissions carry evidence, the path#key the install path was proven in, so
status: verified cites a file instead of asserting one. Rejections carry a
reason and a recheck date. What stays in the queue is what a machine should not
have decided: themes that belong in the sibling registry, and anything with no
description upstream to copy — this registry copies an author's own words and
never writes new ones for them.
SKILL.md files used to sit in that queue too, 163 of them, waiting on a
judgement nobody could make differently: a frontmatter dsh rejects is not a
close call, it is a file dsh ignores. They are decided automatically now. The
pile also hid a bug worth stating plainly — the prover's frontmatter test
required a newline before name:, so --- followed immediately by
name:, the way almost everyone writes it, did not match, and neither did any
CRLF file. 84 of those 163 were real skills the registry had already found
and was refusing to list. The check is now ported from dsh's loader rather
than approximated.
Hand-triage is still welcome and always wins: move the entry into
plugins.json yourself (candidate metadata is a lead, not a record) or record
it in data/rejected.json with a one-line reason.
Topic sweeps are exhaustive. GitHub caps search at 1000 results per query and
dsh-plugin passed that on 2026-08-14, so the sweep slices a topic by creation
date, an over-cap day by star count, and an over-cap zero-star day by repo size
— that last axis because the zero-star bucket has no finer star slice below it
and is exactly where a spam wave lands: on 2026-08-17 it held 957 of the 1000
the API will return. Each topic reports examined N/total, and a slice that
saturates every axis says how many repos it could not read, because a
saturating sweep and a quieting ecosystem produce the same falling batch sizes.
Rejections expire
Two kinds of rejection live in the ledger, and they are not the same thing:
- Judgment. "Curated list, not an installable extension." "Standalone agent
platform, topic-riding." These are permanent: omit
recheckAfter. - Snapshot. "No dsh install path" is a fact about the day it was checked. A
project that ships its manifest a week later would be buried forever by a
permanent row. These carry
recheckAfter, and discovery re-queues them once that date passes — 21 days for a missing install path, 7 for a repo that was unreadable, 90 for one that vendors the runtime instead of extending it.
To appeal a live rejection, delete its ledger row in the same PR that adds the entry.
Field ownership
Human judgment always wins over automation. tags picked by a person during
triage or review are never overwritten by the watch — automation may only fill
fields that are empty. featured is hand-curated and off-limits to automation
entirely. stars, starsUpdated, and pushedAt are the reverse: machine
fields refreshed by scripts/stars.mjs, not worth editing by hand.
Verification sweeps
dsh is a developer preview with promised breaking changes, so entries rot.
Re-verification PRs that only bump lastVerified / verifiedAgainst, or flip
status to broken with a one-line reason, are always welcome.
npm run prove re-reads every listed entry and rewrites its evidence,
lastVerified, and verifiedAgainst. An entry it cannot prove drops to
status: unverified and loses its evidence — it is never deleted, because an
unreachable repo and a dead one look identical from here. Repos that 404 are
named in the run's output rather than acted on; confirm across two runs before
removing a row, since a repo can 404 for hours and come back.
scripts/validate.mjs fails any non-official entry claiming status: verified
without evidence, which is what keeps the status from drifting back into
decoration.
npm names are checked against npm
npm is the one field in an entry that is an instruction rather than a fact —
it renders as npm i <name>, and the spam gate accepts "a published npm
package a profile can depend on" as an install path. So it has to be published.
On 2026-08-21 it was not. 298 of 582 npm names 404ed at
registry.npmjs.org, and all 298 were written on 2026-08-14 by a bulk pass that
read name out of the repo's own package.json — the name an author would
publish under, not evidence that they had.
npm run npm-check asks npm about every name in both directions each run:
- a listed name that 404s moves to
data/unpublished.jsonand the field is dropped from the entry, - a parked name that resolves is restored to the entry it came from,
- a name that times out changes nothing. An unanswered question is not a "no", and the run says how many it could not reach.
Nothing is deleted, because an author who has not published yet may publish next week and the parked name is how we would notice. Re-adding a parked name by hand fails validation; publish the package, or run the check.