Running a Private Claude Code Plugin Marketplace
2026-05-26 - 10 min read
You wrote a Claude Code skill yesterday. Three teammates want it by Friday. Here's how to package skills as plugins and share them via tagged releases.

Last verified against Claude Code v2.1.150 on 2026-05-26. We re-verify quarterly; owner: Dan.
What you're building
You wrote a Claude Code skill that turned a half-day task into a five-minute one. You used it three times before noticing two of your teammates were still doing the half-day version. You sent them the skill file in Slack. Now there are three copies in three states of drift, and someone has already complained that "the skill stopped working" — it didn't; they're running an old version.
A private plugin marketplace solves this. One git repo, one tag, one command for teammates to pull. The setup is small enough to do in an evening; the impact compounds for every skill you add after the first one.
The topology has four parts:
- Marketplace repo — a git repo with
plugins/<name>/...directories, a root manifest, and per-plugin manifests. This is the single source of truth for all your team's shared plugins. - Local cache —
~/.claude/plugins/cache/<repo-name>/<plugin>/<version>/on each developer's laptop. Claude Code populates this from the marketplace repo. - Per-directory enablement —
.claude/settings.local.jsonat the directory where you want plugins active. This file is usually gitignored or committed depending on whether the scope is personal or shared. - Tag-based rollout —
git tag <plugin>/<version>, push the tag, teammates fetch and checkout. That's the entire distribution mechanism.
That's the whole shape. The rest of this guide is the executable form.
Prerequisites
- Claude Code ≥ v2.1.150 (this guide is verified against the version in the header).
- git ≥ 2.30 (you'll use lightweight tag push semantics).
- A repo host you control — a private GitHub repo, an internal Gitea, anything that supports tags.
- macOS or Linux. Windows via WSL should work but isn't verified here.
- Basic terminal comfort. You'll be running git commands and editing JSON files.
The setup is "one sitting" if you already have the prerequisites. The polish — multiple plugins, hooks, MCP server configs — takes longer.
Repo skeleton
We're creating a marketplace repo with one starter plugin called hello-world. Its only job is to confirm the install loop works. Once you can see hello-world:hello in your plugin list, you know the plumbing is right. Resist the urge to build your "real" first plugin before this smoke test passes — the install loop has a few failure modes that are much easier to debug with a throwaway plugin than with a plugin you care about.
Here's the directory structure we're building:
my-claude-marketplace/
├── plugins/
│ └── hello-world/
│ ├── plugin.json
│ └── skills/
│ └── hello/
│ └── SKILL.md
└── marketplace.jsonThe root marketplace.json is the registry — it tells Claude Code what plugins live in the repo and where to find them. As you add more plugins, you add entries here; the path is relative to the repo root:
{
"name": "my-claude-marketplace",
"plugins": [
{
"name": "hello-world",
"path": "plugins/hello-world"
}
]
}Each plugin has its own manifest. The version field is what Claude Code uses to key the local cache — when you bump the version and push a new tag, the cache path changes, so Claude Code fetches the new contents rather than serving the stale cached version:
{
"name": "hello-world",
"version": "0.1.0",
"description": "Smoke-test plugin to verify the marketplace install loop works.",
"skills": ["skills/hello/SKILL.md"]
}The skill file itself is a markdown file with YAML frontmatter. This one is intentionally minimal — its entire job is to be findable and invocable:
---
name: hello-world:hello
description: Print a greeting. Used to verify the marketplace is wired up correctly.
---
# Hello
Print: "hello from the marketplace — install loop works."
That's it. This skill exists so you can confirm the marketplace
finds, loads, and exposes the plugin's skills.Create the repo, commit the files, and push the first version tag. The env-var preamble makes the placeholder explicit — replace REPO_URL with your own remote:
REPO_URL="git@example.com:your-org/my-claude-marketplace.git" # replace with your own
mkdir ~/work/my-claude-marketplace
cd ~/work/my-claude-marketplace
git init
git add .
git commit -m "initial: hello-world plugin"
git remote add origin "$REPO_URL"
git push -u origin main
git tag hello-world/0.1.0
git push origin hello-world/0.1.0Success signal: the push completes without error. Verify the tag landed with git ls-remote --tags origin — you should see refs/tags/hello-world/0.1.0 in the output.
Installing the marketplace
Add the marketplace to Claude Code's plugin registry. This is a one-time setup step per developer — once the marketplace is registered, enabling individual plugins in a directory is just a JSON file. The exact subcommand may shift across Claude Code versions — check claude --help | grep -i marketplace first if anything below doesn't parse.
REPO_URL="git@example.com:your-org/my-claude-marketplace.git" # same URL as before
claude plugin marketplace add "$REPO_URL"Success signal: the command returns without error. Confirm with claude plugin marketplace list — your marketplace name should appear in the output.
Now enable the plugin in a test directory. We're creating a throwaway directory so this verification is completely isolated:
mkdir -p ~/work/marketplace-test/.claudeWrite the scoping file that opts this directory into the hello-world plugin:
{
"plugins": ["hello-world"]
}Then verify:
cd ~/work/marketplace-test
claude plugin listSuccess signal: hello-world appears in the list, and hello-world:hello is shown as an available skill.
If the plugin doesn't appear, the most common cause is the cache-refresh footgun covered in the next section. Bail out of any active Claude Code session in that directory and try again from a fresh session start.
Versioning and team rollout
Plugins are versioned by git tag using the convention <plugin-name>/<semver>. The producer side and the consumer side each have a two-step flow.
Producer side — tag the new version and push the specific tag:
MARKETPLACE_REPO="$HOME/work/my-claude-marketplace"
PLUGIN="hello-world"
VERSION="0.2.0"
TAG="$PLUGIN/$VERSION"
cd "$MARKETPLACE_REPO"
git tag "$TAG"
git push origin "$TAG" # push the specific tag, not git push --tagsSuccess signal: git ls-remote --tags origin | grep "$TAG" shows the new tag.
Consumer side — fetch the tags and checkout the specific one:
MARKETPLACE_REPO="$HOME/work/my-claude-marketplace"
TAG="hello-world/0.2.0"
cd "$MARKETPLACE_REPO"
git fetch --tags origin
git checkout "$TAG"The git checkout "$TAG" creates a detached HEAD — this is intentional for pinning the team to a specific version. To return to the marketplace's main branch later: git checkout main. If you want to develop new plugins, do that work on main; tag-checkouts are for consumption only.
Success signal: git status shows HEAD detached at hello-world/0.2.0. The plugin cache refreshes on the next Claude Code session start in any directory that has that plugin enabled.
This replaces the ambiguous git pull --tags (which both fetches and merges based on the local branch's tracking state). Fetch-then-checkout is deterministic.
The cache-refresh footgun
If you run /plugin update from inside Claude Code while your marketplace repo is on a WIP branch, Claude Code caches the WIP-branch contents under the version tag. The cache now disagrees with what the tag actually points to — and your teammates will see different behavior than you do.
Symptoms: skills behave inconsistently across teammates who all "pulled the same tag." One developer gets the old behavior; another gets yours. The version number matches, but the contents don't.
The safe recovery is inspect-then-move. Don't delete — move to trash so you can recover if needed:
STALE_DAYS=30
CACHE_ROOT="$HOME/.claude/plugins/cache/my-claude-marketplace"
TRASH="$HOME/.Trash" # macOS; on Linux use ~/.local/share/Trash/files
# Inspect first — always.
find "$CACHE_ROOT" -mindepth 2 -maxdepth 2 -type d -mtime "+$STALE_DAYS" -print
# If the listing is what you expect, move (don't delete) to trash:
find "$CACHE_ROOT" -mindepth 2 -maxdepth 2 -type d -mtime "+$STALE_DAYS" -print0 | xargs -0 -I{} mv -v "{}" "$TRASH/"Success signal: the find output shows the directories you expected to see, and the mv command moves them without error.
Prevention is one rule: never run /plugin update while your marketplace repo is on a WIP branch. Get back on a clean tag or main first.
Rollback
To roll back to an earlier version, the same fetch-then-checkout flow applies:
MARKETPLACE_REPO="$HOME/work/my-claude-marketplace"
cd "$MARKETPLACE_REPO"
git fetch --tags origin
git checkout "hello-world/0.1.0"Success signal: git status shows HEAD detached at hello-world/0.1.0.
Per-directory scoping
The reason to use per-directory scoping instead of enabling every plugin globally is clutter. Different work demands different tooling. A plugin built around one client's data pipeline has no business showing up in sessions for an unrelated project. Global plugin sprawl also creates confusion about which skills are available in which context — and when a skill has the same name in two different plugins, the wrong one can win silently.
The mental model is: the directory you open Claude Code in is the scope. Whatever settings.local.json file Claude Code finds there (or inherits from a parent directory) determines which plugins load. That's the entire model — no conditionals, no environment variables, no runtime toggling.
A kitchen-sink workspace root enables everything relevant to that workspace:
{
"plugins": ["drycodeworks", "dry-internal", "dry-harness", "hello-world"]
}A per-client subdirectory scopes down to just what that client's work needs:
{
"plugins": ["drycodeworks", "example-client"]
}Replace example-client with your own per-client plugin name.
The silent-scoping footgun
A typo in a plugin name disables the plugin without warning. There's no error message — the plugin is simply absent. Engineers who don't internalize the scoping model will think "the skill is missing" when in fact the scope file has a typo, a missing comma, or a mismatched name.
Verify the scope is right after any change to a settings.local.json:
cd ~/work/example-client
claude plugin listSuccess signal: the plugins you expect to see are listed. If example-client (or whatever you named your plugin) isn't in the list, the scope is wrong — check the JSON for typos, missing commas, or mismatched plugin names.
The iteration loop
The day-to-day rhythm for developing and shipping a plugin change.
Step 1: Edit on a branch
New skill, new hook, change to an MCP server config — all on a branch in the marketplace repo, never directly on main. main should always be in a state you'd be comfortable tagging.
Step 2: Test locally
Enable the in-development plugin in a throwaway directory (~/work/scratch/) and verify the skill behaves the way you want. Point the scope file at the plugin by name; Claude Code picks up the branch contents from the local checkout. Keep the throwaway directory's scope file out of the marketplace repo — it's for testing, not distribution.
Step 3: Bump the version
Increment the plugin's version field in plugin.json. Use semver — bump patch for fixes, minor for additions, major for breaking changes. Merge the branch to main before tagging.
Step 4: Tag and push
MARKETPLACE_REPO="$HOME/work/my-claude-marketplace"
PLUGIN="hello-world"
VERSION="0.2.1"
TAG="$PLUGIN/$VERSION"
cd "$MARKETPLACE_REPO"
git tag "$TAG"
git push origin "$TAG"Success signal: git ls-remote --tags origin | grep "$TAG" shows the new tag.
Step 5: Team picks up the new version
Each teammate runs git fetch --tags origin && git checkout "$TAG" in the marketplace repo, or wraps that in a shell alias. The cache refreshes on the next session start in any directory that has that plugin enabled. If you have a team standup cadence, "pull the new marketplace tag" is a five-second step that can live in the same checklist as pulling main on project repos.
Don't run /plugin update while your marketplace repo is on a WIP branch. The cache will pin to the WIP contents under the published version tag, and your teammates will see different behavior than you do.
What to put in your first plugin
Three starter plugins worth building, in rough priority order:
- A house-style skill. Encode your team's conventions — how you write commits, how you phrase PR descriptions, how you structure plan files. The next teammate's first PR is suddenly closer to your norms without anyone having to explain it.
- A "before-commit" hook. Run your linter, your formatter, your typechecker. Don't let Claude push a commit that breaks any of those — fail loudly in the hook and let the AI fix it before the PR opens.
- An orchestrator skill. A single slash command that dispatches a multi-step task with a self-contained prompt — the dispatch pattern the opinion piece walks through. This is the leverage move: the encoding happens once, and every subsequent invocation for a similar task inherits the same approach without reconstruction from memory.
Build one. Use it for a week. Build the next one when you notice you're repeating yourself.
For the deeper how-to on writing the skills themselves — frontmatter, decision flow, error handling, anti-patterns — see Building Custom Claude Code Skills for Infrastructure Automation. This guide is the marketplace layer; that one is the skill-craft layer beneath it.
Where to go next
For the why — the argument that this pattern is worth committing to in the first place — see the companion opinion piece: The Harness and the Appliance.