Claude Code Plugin Marketplace Curation: What Earns a Slot

Photo by Diliff via Wikimedia Commons (CC BY 2.5)
Admit a plugin when it encodes a decision the team has already made and has had to explain more than once, such as a domain invariant a new engineer cannot infer from the code. Decline anything that merely wraps a command nobody struggled with, because it adds a name to remember without encoding a choice. If a linter rule or a CI job can enforce the same thing deterministically, that is a stronger home for it than a catalogue entry.
A plugin's name is its stable identifier, referenced in enabledPlugins and in install commands, so deleting the entry outright gives every existing user a plugin-not-found error. Announce the retirement first by changing displayName and description while leaving name alone, then set defaultEnabled to false so new installs arrive disabled. Only after the grace period, remove the entry and add a top-level renames map that points the old name at its replacement, or at null when the plugin is gone.
An unmaintained internal plugin is worse than no plugin, because a stale skill looks identical to a fresh one and the model reads both with the same authority. Put a person rather than a team in the author object, since a team is how an entry quietly ends up unowned. The metadata object is free-form and not read by Claude Code, which makes it the right place for an owner name and a review date.
Fewer than feels generous. A plugin's listing text, meaning its skill and agent descriptions and command names, is always-on context in every session whether or not anything fires, so an unused entry is charged to everyone forever. Run claude plugin details to see the projected always-on cost per entry, and set defaultEnabled to false on specialist plugins so they install disabled until somebody opts in.
Claude Code checks for marketplace and plugin updates after a session starts, with a random delay of up to ten minutes, and the running session keeps the versions it loaded at launch. A git-sourced entry with no version declared resolves its version from the commit SHA, so any push to the marketplace repository is an update the team receives. Declare version on every entry and bump it on each change, so the difference is something a person can compare.

Photo by Diliff via Wikimedia Commons (CC BY 2.5)
Key Takeaway
An internal Claude Code plugin marketplace is a policy document: every entry is an instruction the whole team's agents follow. Admit a plugin only when it encodes a decision the team has already made, give each entry a named maintainer, and retire it through the marketplace renames map so no existing install breaks.
The question that started this policy was four words long: which of these should I install? A colleague had added our internal marketplace, opened the plugin browser and found a list long enough that choosing from it had become its own task. Every entry carried a description. Not one of them told him whether anybody still used it, who to ask about it, or what it would change about the way his agent behaved on our code.
I maintain a public Claude Code marketplace that has grown past sixty entries, and a much smaller internal one. The public catalogue can afford to be generous, because installing from it is one person's choice. The internal one cannot, because everything in it changes how a teammate's agent behaves on code we all ship. This post is the editorial policy I run the internal one by. It deliberately skips the manifest format and the publishing mechanics, which the reference documents and an earlier post already cover.
Every entry in an internal marketplace is an instruction that executes on somebody else's machine, in somebody else's session, against code you both own. A skill is prose the model reads as guidance. A hook is a shell command that fires whether the agent agrees with it or not. A subagent carries its own system prompt and its own tool restrictions. Publishing one is closer to merging a change to a shared linter config than to adding a link on a wiki.
That is why the interesting question was never how to publish an entry. The manifest is one JSON file, and the tooling validates it for you: claude plugin validate checks schema and syntax and exits non-zero when they are wrong. What it cannot check is whether the plugin encodes something true, still current, and worth the whole team reading. That part is entirely editorial, and when nobody owns the editing a catalogue degrades in one predictable direction, which is upward.
The test I apply is whether the plugin encodes a decision the team has actually made and has had to explain twice. A closed-period rule in the finance module is a decision: it was argued about, getting it wrong has consequences in the ledger, and no new engineer can infer it from the code. A slash command that runs the test suite is not a decision. Nobody struggled with it, nothing was chosen, and the plugin's only contribution is one more name to remember for something everyone already types.
The fourth test rejects the most proposals and is the one people argue with, so it is worth being blunt about the reason. A deterministic check runs whether or not the agent thinks it is relevant. An instruction only lands if it reaches the model at the moment it matters. So the honest answer to a good number of proposals is that the rule belongs in CI, and putting it in the catalogue as well would only make the weaker copy the one people trust.
Ask the proposer to name the pull request, review comment or incident the plugin would have prevented. If they can point at one, the entry encodes something real. If the example is hypothetical, the entry will be too, and it will still cost every session it loads into.
Popularity is not a review. A widely installed bundle solves somebody else's problem convincingly, which is exactly what makes it tempting and exactly what disqualifies it by default: it encodes their decisions, not ours. The security audit of a third-party bundle is a separate pass with its own checklist, and I run it separately. The editorial question is narrower and easier to skip. Will this bundle still say what we want it to say in six months, once its author has changed it?
Two facts from the plugin reference make that answerable rather than a matter of temperament. The command claude plugin details prints a plugin's component inventory alongside two token figures: always-on, which the plugin's listing text adds to every session whether or not anything fires, and on-invoke, which a component costs when it actually runs. And a git-sourced entry with no version declared resolves its version from the commit SHA, so every push upstream is an update your team receives. Those are the two commitments an outside entry asks for: a standing context cost, and an unbounded upstream.
# Two questions an outside bundle answers before it gets an entry.
# 1. What does it add to every session, whether or not anybody invokes it?
claude plugin details security-review@vendor-tools
# Component inventory
# Skills (2) threat-model, review-diff
# Agents (0)
# Hooks (1) PreToolUse (harness-only — no model context cost)
# Projected token cost
# Always-on: added to EVERY session by the listing text alone
#
# Always-on is the catalogue's number. It is charged per session, per member,
# for as long as the entry exists — including to everyone who never uses it.
# On-invoke is printed per component rather than as a plugin total, because a
# real session fires only a subset. Do not add the column up.
# 2. Does an entry already do this? Read the shape once, then search it.
# --available is what makes the list include plugins you have NOT
# installed, which is the half a duplication check cares about, and it
# only works together with --json.
claude plugin list --available --json > /tmp/catalogue.json
jq '.[0]' /tmp/catalogue.json # the fields your version emits
grep -i -e review -e threat /tmp/catalogue.jsonAn unmaintained internal plugin is worse than no plugin at all. A wiki page nobody has touched in a year looks out of date; a skill nobody has touched in a year looks exactly like a skill somebody revised last week. The model reads both with the same authority, the agent acts on both with the same confidence, and the reader has no signal to distinguish them. Stale guidance that everybody trusts is the most expensive artefact a catalogue can hold.
So an entry does not land without a named maintainer, and the marketplace file is where that name lives rather than a spreadsheet. The author object is a real schema field taking name, with optional email and url. The metadata object is documented as free-form data for your own fields that Claude Code does not read, which makes it the correct place for the facts that matter to us and to nobody else: who owns this, when it was last reviewed, and when it is due again. A person, never a team, because a team is how an entry ends up unowned without anybody noticing.
// marketplace.json — the two places a maintainer's name can actually live.
{
"name": "erp-invariants",
"source": "./plugins/erp-invariants",
"description": "Closed-period and branch-scoping rules for the finance module",
// Declared here so a later behaviour change has a number to compare.
"version": "1.4.0",
// A real schema field: name is required, email and url are optional.
// This is the name a confused teammate needs, so it is not a mailing list.
"author": { "name": "Matthews Wong", "email": "[email protected]" },
// Documented as a free-form object for your own fields, which Claude Code
// does not read. That is exactly what makes it safe for governance data:
// it can never change how the plugin behaves, only who answers for it.
"metadata": {
"owner": "matthews",
"reviewedOn": "2026-09-02",
"reviewDue": "2026-12-01",
"decisionRecord": "docs/adr/0031-closed-period.md"
}
}# claude plugin validate checks schema and syntax. It has no opinion about
# ownership, so that gate is ours to write. One jq expression in CI on the
# marketplace repository, run on every pull request that touches the file.
jq -e '
.plugins
| map(select((.author.name // "") == "" or (.metadata.owner // "") == ""))
| if length == 0 then true
else "unowned entries: " + (map(.name) | join(", ")) | halt_error(1)
end
' .claude-plugin/marketplace.json
# unowned entries: legacy-linter, screenshot-helper
# The same run, second gate: nothing may sit past its own review date.
jq -e --arg today "$(date -u +%F)" '
.plugins
| map(select((.metadata.reviewDue // "9999-12-31") < $today))
| if length == 0 then true
else "review overdue: " + (map(.name) | join(", ")) | halt_error(1)
end
' .claude-plugin/marketplace.json
# Then the schema check, which is the cheap one and still worth failing on.
claude plugin validate .
A plugin's name is its stable identifier. Users reference it in enabledPlugins, in pluginConfigs and in install commands, so renaming or deleting an entry breaks every existing install with a plugin-not-found error. Changing only the label people see is free: set displayName and leave name alone. Actually removing the entry needs the one field built for this, a top-level renames map from a former name to its current name, or to null when the plugin is gone.
// Retirement in the order that breaks nobody. Three commits, not one.
// 1. Announce it in the catalogue itself, where the reader already is.
// displayName is only the label in the UI; name is the identifier that
// every existing install references, so it must not move.
{
"name": "legacy-linter",
"displayName": "legacy-linter (deprecated, removal 2026-11-01)",
"description": "Superseded by code-formatter. Do not adopt for new work.",
"source": "./plugins/legacy-linter"
}
// 2. Freeze it. defaultEnabled false means a new install arrives disabled,
// and the marketplace entry takes precedence over the same field in the
// plugin's own manifest — so this decision is the catalogue owner's, not
// the plugin author's. An enabledPlugins entry a user already has wins
// over it and persists across updates, so this flips nobody's session.
{
"name": "legacy-linter",
"defaultEnabled": false,
"source": "./plugins/legacy-linter"
}
// 3. Only after the grace period: drop the entry AND record the removal.
// Without the renames line, every remaining install reports
// plugin-not-found, which reads to the user as a broken tool.
{
"name": "acme-tools",
"owner": { "name": "Platform" },
"plugins": [
{ "name": "code-formatter", "source": "./plugins/code-formatter" }
],
"renames": {
"legacy-linter": null,
"formatter": "code-formatter"
}
}
// null -> Claude Code drops the stale key and reports the removal
// a name -> it loads under the new name and rewrites the key once, in the
// user, project and local scopes, so the notice appears once
// remote -> a github or npm source reports plugin-cache-miss after the
// rename, and the user runs install once to fetch it againTwo documented properties of that map decide the policy around it. It is append-only history, so old entries stay in place long after you expect everyone to have migrated, and a second rename gets a second entry rather than an edit to the first, because Claude Code follows the chain. And claude plugin validate rejects a map whose chain forms a cycle or fails to terminate at null or at a plugin still listed, which turns a retirement into something reviewable in CI instead of by inspection.
Managed and policy settings are read-only to Claude Code, so a plugin your organisation enabled there cannot be rewritten automatically. The renamed plugin still loads every session, but the rename notice recurs until an administrator updates enabledPlugins in the managed file. Retiring a managed entry is two changes in the right order, or the whole team gets a notice they have no way to clear.
Thirty entries where four get used is not a documentation problem and no README fixes it. It is a cost paid twice. Once by every reader who has to assess twenty-six irrelevant plugins to reach the one they need, and once per session by every member of the team, because a plugin's listing text, meaning its skill descriptions, agent descriptions and command names, is always-on context whether or not anything ever fires.
So the catalogue has a ceiling, and proposing the twelfth entry means arguing that it beats one of the eleven already there. Two mechanisms make the ceiling liveable. Setting defaultEnabled to false on a marketplace entry installs a specialist plugin disabled until somebody opts in, and the marketplace entry takes precedence over the same field in the plugin's own manifest, so a catalogue owner can make that call for the whole team. And claude plugin prune, aliased autoremove, lists the auto-installed dependencies no remaining plugin requires and asks before removing them, which is worth running with dry-run after any retirement.
The command claude plugin details is what settles an argument about catalogue size. It prints the component inventory and the projected always-on cost for one plugin, computed with the count_tokens API for your active model and falling back to a character estimate when that is unreachable. Run it across every entry once a quarter and the price of the catalogue stops being a matter of opinion.
The failure has a signature. Somebody mentions that the agent has started doing something odd, everybody speculates about the model, and nobody connects it to an update they never saw. That is not carelessness, it is how the update path is built. Claude Code checks for marketplace and plugin updates after your session starts, with a random delay of up to ten minutes, and the running session keeps the versions it loaded at launch. You get a notification prompting you to run reload-plugins, or the new versions simply arrive at the next launch.
Two defaults make that silent arrival likely in exactly the configuration a team is most likely to run. Background auto-update is off for third-party marketplaces unless somebody sets autoUpdate to true on the extraKnownMarketplaces entry, which administrators do, precisely so nobody is stuck on a stale catalogue. And a git-sourced entry with no version declared resolves its version from the commit SHA, which the reference recommends as the simplest setup for an actively developed internal plugin. Both choices are reasonable. Together they mean a one-line edit to a skill file reaches everyone's next session with no version number for anybody to compare.
# The change is invisible because there is nothing to compare. Give it a
# number, and the incident becomes a diff instead of a conversation.
claude plugin list --json | jq -r '.[] | [.name, .version] | @tsv'
# erp-invariants@acme-tools 1.4.0
# release-guard@acme-tools 0.9.2
# What actually changed, from the only place that keeps a record: the repo
# behind the marketplace. Skill prose is the diff that matters here, and it
# shows up in no lock file, because there is no lock file.
git -C ~/work/acme-claude-plugins log --oneline -- plugins/erp-invariants
git -C ~/work/acme-claude-plugins diff v1.3.0..v1.4.0 -- plugins/erp-invariants
# Bisect the behaviour rather than re-reading the prose. Disable the suspect
# and reproduce the odd turn. This also fails when another enabled plugin
# depends on it, which is itself the answer to: is this entry load-bearing.
claude plugin disable erp-invariants@acme-tools
# And a baseline when nobody can name a suspect: turn the catalogue off for
# one session, reproduce, then re-enable the four entries people really use.
claude plugin disable --all
This is short enough to paste into a pull request template, which is where it works best, because the decision is made once by the proposer and reviewed once by whoever owns the catalogue.
The step that gets skipped is the third, and it is the one that decides whether the catalogue is still true in a year. Everything else can be fixed later by whoever owns the entry, which is only possible if somebody does.
The rule I carry out of this is that a marketplace entry is a standing instruction with a person's name on it, or it is not an entry. The review bar, the token budget, the version discipline and the renames map all follow from that one sentence. They are the mechanisms that keep a catalogue smaller, and more honest, than the enthusiasm that fills it.
Sources and further reading