Repos, Not Catalogs: Where Plugin Identity Actually Lives
Abstract
The default advice for distributing Claude Code plugins is to maintain a single central marketplace per author. This minimizes distribution overhead and groups your work in one discoverable catalog. The defaults look right until you ask what GitHub’s social infrastructure actually rewards. Stars, forks, watchers, issues, releases, and search all aggregate at the repository level, not the catalog level. A plugin that lives as one row in a multi-plugin catalog inherits the catalog’s identity, not its own. The argument cuts cleanly: single-product plugins belong in self-marketplaces; toolkit clusters belong in central catalogs. The deciding factor is brand identity, not distribution overhead.
The Default Framing
A Claude Code marketplace is a git repository whose .claude-plugin/marketplace.json lists one or more plugins for users to install. The official documentation walks through a marketplace with a single plugin as the basic example, but in practice the dominant pattern is one marketplace per author listing many plugins. The Anthropic-curated claude-plugins-community is the canonical instance; most third-party authors mirror the shape.
This pattern has clear ergonomic advantages. One claude plugin marketplace add owner/repo makes the entire catalog available. Users install many plugins from one source. The author keeps one marketplace.json in sync. New plugins join the catalog with a single PR. The friction story is tidy.
The story is also incomplete. It treats distribution overhead as the primary cost to minimize, and it assumes the author’s plugins are best discovered together.
What GitHub Actually Aggregates
GitHub is not a neutral hosting layer. It is a social platform whose features aggregate around the repository as the unit of identity. Stars accrue to a repo. Forks fork a repo. Watchers watch a repo. Issues and PRs are scoped to a repo. The releases page is per repo. The README that visitors land on is the repo’s. Search results return repos. Forks, dependent projects, and citations all carry the repo URL.
When a plugin lives as one entry in a multi-plugin catalog, every one of those surfaces points at the catalog rather than the plugin. The plugin gets a paragraph; the catalog collects the stars. The plugin’s bugs share an issue tracker with the catalog’s other plugins. The plugin’s release notes live in the catalog’s release timeline, mixed with whatever the catalog’s author shipped that week. A visitor searching for the plugin’s name lands on the catalog and has to navigate inward.
| Surface | Catalog-listed entry | Self-marketplace |
|---|---|---|
| GitHub search “plugin-name” | Catalog repo (then navigate inward) | Direct hit on plugin repo |
| Stars, forks, watchers | Aggregate at catalog | Aggregate at product |
| Issues, PRs | Mixed with N other plugins | Product-scoped |
| Releases page | Catalog’s mixed timeline | Product’s own timeline |
| README as landing page | Catalog’s README | Product’s README |
| External links and citations | ”See entry Y in catalog X” | Direct product URL |
| Forking | Clone the whole catalog | Clone the one product |
None of this is fatal in isolation. Catalog-listed plugins work fine. But each row of the table dilutes the plugin’s standing as a product. A plugin that ships in psychquant-claude-plugins/plugins/che-transport-mcp/ reads as “one of Che’s plugins”; a plugin that lives at PsychQuant/che-transport-mcp/ reads as a thing. The two framings produce different behaviors from users, contributors, and search engines.
The Twin-Bump Symptom
The brand-identity argument is the upstream cause. Downstream of it is a coordination problem that shows up immediately when a plugin’s binary and its plugin shell live in different repos.
A typical Claude Code MCP plugin has two artifacts: a binary (often a signed Swift or Go executable hosted on GitHub Releases) and a plugin shell (the .mcp.json, the wrapper script, skill markdown files, and a manifest). When the binary lives in repo A and the plugin shell lives in catalog repo B, every binary release requires:
- Bump version in repo A’s
Version.swift(or equivalent) - Tag, sign, notarize, release the binary in repo A
- Bump the matching
binaryVersionfield in repo B’splugins/<name>/.claude-plugin/plugin.json - Bump the matching catalog entry in repo B’s
.claude-plugin/marketplace.json - Commit and push repo B
- Sync the marketplace with
claude plugin marketplace update
The actual change is “new binary.” The bureaucracy that turns that change into a release for users is six steps across two repos. The forgetting modes are predictable: forget the binaryVersion bump and the wrapper script still points at the old release; forget the marketplace.json bump and claude plugin update reports no change; commit one of the bumps to the wrong branch (easy when you have feature branches open in either repo for other work) and the catalog ships a broken source pointer until the next push.
Co-locating the plugin shell with the binary source folds those six steps into one PR. The binary, the plugin shell, and the marketplace catalog (now listing only this one plugin) all live in the same repo. A binary release is one commit that bumps both versions, plus make release-signed && gh release create and you are done. The friction story improves, but the deeper reason to do this is what each version of the layout means on GitHub: one product, one repo, one identity.
Where Catalogs Still Win
The argument above is not a universal preference for self-marketplaces. There is a real class of plugin where the central catalog is the correct shape, and the test is sharp.
A central catalog is the right shape when the catalog itself is the brand.
Two patterns satisfy this test:
Community or curated marketplaces. claude-plugins-community is Anthropic’s curated catalog of third-party plugins. The value proposition is the curation: someone reviewed each plugin, the bundle is what users want. The individual plugins remain identified with their authors; the catalog earns its own identity as a trusted entry point. The same logic applies to a corporate internal marketplace (“ACME’s approved plugins”), a community-of-practice marketplace (“psychology research tools”), and any other case where users add the catalog because they trust whoever assembled it.
Toolkit clusters. A toolkit is a set of related plugins that share a problem domain and whose value derives from being available together. plugin-tools (plugin development workflow), ai-docs-guide (navigating AI documentation), teaching-toolkit (teaching workflows), doc-tools (documentation utilities), issue-driven-dev (a multi-skill IDD workflow), data-engineering (an Airflow / dbt cluster). Users who install one usually want the others. Splitting the cluster across repos would force users to add five marketplaces to get one coherent workflow and would scatter cross-cluster discussions across five issue trackers. The cluster’s coherence is itself the product.
The test reduces to one question: would users want plugin X if you removed the other plugins in the same catalog? If yes, X is a standalone product and belongs in its own self-marketplace. If no, X is a cluster member and belongs in the catalog.
A Concrete Example
I recently migrated PsychQuant/che-transport-mcp from a catalog entry to a self-marketplace. The repo now contains the Swift binary source, the plugin shell, and a one-entry marketplace.json catalog. The layout is:
PsychQuant/che-transport-mcp/
├── .claude-plugin/
│ └── marketplace.json # lists this repo's plugin as the sole entry
├── plugin/ # plugin shell (the part users install)
│ ├── .claude-plugin/plugin.json
│ ├── .mcp.json
│ ├── bin/
│ ├── hooks/
│ └── skills/
├── Sources/ # Swift binary source
├── Tests/
└── Makefile
The plugin entry in marketplace.json uses a relative "source": "./plugin" to point at the plugin shell subdirectory. Only that subdirectory is copied into the user’s plugin cache; Sources/, Tests/, and the rest of the build infrastructure stay out. Users install with:
/plugin marketplace add PsychQuant/che-transport-mcp
/plugin install che-transport-mcp@che-transport-mcp
The doubled name (plugin-name@marketplace-name) is slightly awkward. It is a feature, not a bug: it preserves the symmetry of every other install, and it costs nothing once you have typed it.
The central catalog I used to ship from (psychquant-claude-plugins) now lists only what it should: toolkit clusters whose value depends on being grouped. The transport MCP, the keychain CLI, and any future standalone product I ship will each be its own repo. Single source of truth per product, single PR per release, single set of stars per identity.
Decision Defaults
For anyone shipping Claude Code plugins:
- New plugin that is a single standalone product (one binary, one MCP server, one CLI tool): start with a self-marketplace from day one. The repo and the marketplace are the same thing.
- New plugin that is one member of a toolkit you also ship: add it to the toolkit’s catalog. Coherence first.
- Existing plugin in a central catalog that is actually standalone: consider extracting. The migration is straightforward — co-locate the shell with the source, add a one-entry
marketplace.json, switch the central catalog entry to agit-subdirpointer (or remove it entirely if you want one canonical install path). - Existing toolkit cluster: leave it alone. The clustering is doing real work.
The bias should be toward more, smaller marketplaces, because the cost of an extra claude plugin marketplace add is one line on the user’s side, while the gain in repo-level identity compounds over the life of the project.
Coda
The marketplace.json file is sixty lines of JSON. The architectural decision behind where you put it is whether your work shows up as itself or as a row in someone else’s catalog — even when that someone else is you. The default toward one big catalog reads as economical until you notice it is asking the plugin to give up the things GitHub gives to repos.
The argument generalizes beyond Claude Code. Whenever a platform’s social infrastructure is organized around a unit (repo, package, profile, channel), and you are deciding whether to publish as that unit or within a larger container, the question to ask is which surface accrues the identity. The answer is almost always: the unit the platform was designed around.