Part I of a series on how I built an internal Claude Code plugin marketplace for my team.

It’s a pattern that repeats in any development team that starts using Claude Code seriously: everyone ends up building, their own way, their own collection of conventions, shortcuts, and saved prompts so the AI understands the team’s stack. The usual problem: that knowledge lives on each person’s laptop. If someone goes on vacation, joins the team, or simply switches projects, all that context is lost.

I ended up having to solve exactly that, and did what usually works in these cases: turned it into an internal product, even a small one. I put together a repository that’s both an experiment lab and a Claude Code plugin marketplace. With a single command, anyone on the team installs, all at once, every convention, skill, and automation the team has settled on.

The interesting part isn’t the idea itself (installing things from a catalog is nothing new). What’s interesting is that building it forced me to understand every kind of piece a plugin can contain (and I think it’s worth sharing, because almost nobody uses more than one or two).

Here’s the full map, with real (anonymized) examples of what we’ve put together.

1. Your own skills

A skill is, essentially, a set of instructions that Claude loads only when needed (it doesn’t take up context all the time, it shows up when the task matches its description).

The first ones people usually write are the most boring and the most cost-effective: team conventions. How branches are named, the commit message format, what’s expected in a pull request. And a second, more technical layer, with the architectural patterns of whatever stack the team uses: how to structure an endpoint, how to handle errors, which layers should never be skipped.

None of this is glamorous. But it means the same code-review correction stops repeating itself (the convention is written once and Claude applies it on its own whenever someone touches that kind of file).

2. Third-party skills, pointing at other repos

This is the piece fewest people know about, and the one that surprised me most when I found out it existed: a plugin doesn’t have to contain the skill’s code. It can simply point to someone else’s repository.

A typical case: a project’s frontend uses a fairly standard framework from the ecosystem, and there’s already a public open-source repository maintaining skills for that framework. Instead of copying and maintaining your own version, you can reference that repository directly, using a technique that downloads only the folder you need (not the whole repo) and optionally pins a specific version, so someone else’s update can’t break anything without anyone noticing.

The real advantage: when whoever maintains the original repository improves those skills, the team gets the improvements without anyone having to touch anything. And if something breaks, all you need is the exact commit to roll back to.

3. Your own MCP server

MCP is the protocol that lets Claude talk to external systems (databases, internal APIs, whatever) through well-defined tools, not just free text.

When the system you want to connect to is internal and no third-party integration exists, the option is to write your own MCP server. It lives inside the plugin itself (not in a separate directory of the repo, because only what’s inside the plugin gets copied on install) and is referenced with a special environment variable so the paths work no matter where it’s installed.

The nice part of pairing it with a skill in the same plugin: in a single install, the team gets the skill and the tool it needs to run it. Nobody has to configure anything separately.

4. Third-party MCP server

Here the difference is there’s nothing to code (only configuration). If a provider (say, your issue tracker or your internal wiki) already offers an MCP server, your plugin only needs to declare how to start it: the command, the arguments, which environment variables it expects.

The plugin becomes a simple distribution wrapper: instead of everyone hunting down the right package and configuring it by hand, the whole team gets the connection ready to go on install. One rule, though (never credentials in the repository, only the recipe for connecting).

5. Hooks

Hooks are commands that Claude Code itself runs automatically on certain events (before a tool call, after Claude finishes, etc.), not something Claude “decides” to do. They’re the right place for rules that must always hold, not just when the AI thinks it’s a good idea (for example, blocking a destructive command or forcing a code format before accepting a change).

The distinction matters more than it seems: a skill is a suggestion Claude can follow or not depending on context; a hook is a barrier that always runs. If something must always happen, it belongs in a hook, not a skill.

6. Agents (subagents)

Besides skills, a plugin can include subagent definitions: specialized profiles with their own toolset and their own judgment, built for specific tasks (reviewing code, exploring a large repository, drafting a plan). They’re invoked from the main conversation and return a result, without loading all that specialized context into the everyday conversation.

7. The catalog itself (the “marketplace”)

And above all of this sits a catalog file: the list of which plugins exist, where they’re installed from, and what version they’re on. It’s the piece that turns “a folder of scripts in my repo” into something the rest of the team can discover and install with two commands, without having to ask you directly how things were set up.

Under the hood: what this looks like in code

For anyone who wants something more concrete than theory, here’s the actual structure of a plugin in this marketplace:

plugins/my-plugin/
├── .claude-plugin/plugin.json   # manifest: name, version, dependencies
├── skills/<name>/SKILL.md       # own skills → /my-plugin:<name>
├── agents/<name>.md             # specialized subagents
├── hooks/hooks.json             # lifecycle hooks
└── .mcp.json                    # MCP servers the plugin brings

All of that hangs directly off the plugin root (nothing inside .claude-plugin/ except the manifest itself). The practical reason: on install, Claude Code copies the entire plugin directory into a local cache, so any path pointing outside that folder simply doesn’t exist on someone else’s machine. That’s why internal paths are referenced with a variable like ${CLAUDE_PLUGIN_ROOT} instead of paths relative to the repo.

The catalog (marketplace.json) is what decides whether an entry is “ours” or “third-party.” An entry of our own looks this simple:

{
  "name": "team-dx",
  "source": "./plugins/team-dx",
  "version": "0.3.0"
}

An entry referencing another team’s public repository, without copying anything, is different: instead of source as a path, it’s an object describing where to pull just one folder from:

{
  "name": "team-dx-frontend",
  "source": {
    "source": "git-subdir",
    "url": "https://github.com/someone/skills.git",
    "path": "skills",
    "ref": "main"
  },
  "skills": ["./vue", "./pinia", "./vitest"]
}

ref: main means we follow the branch, without pinning a version (we get whoever maintains that repo’s improvements without reviewing them first). That’s a deliberate decision, not an oversight: in exchange, we keep the last known-good commit in metadata (a field Claude Code ignores, but we do read), in case an update breaks something and we need to freeze that reference with an exact sha instead of ref.

For your own MCP, the only difference from a normal server is where it lives and how it’s referenced:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["${CLAUDE_PLUGIN_ROOT}/servers/my-server/index.js"]
    }
  }
}

For third-party MCP, the same file, but with nothing of our own to run (just the command the provider documents):

{
  "mcpServers": {
    "issue-tracker": {
      "command": "npx",
      "args": ["@provider/mcp-server"]
    }
  }
}

And something that isn’t obvious until you run into it: the tools a plugin’s MCP server exposes end up namespaced (mcp__plugin_<plugin-name>__<server>__<tool>) and if you’re writing a hook that should react only to that tool, the matcher has to use that full name. Putting the server’s “short” name in the hook simply never fires.

Lastly, the rule that demands the most discipline and gets seen the least: the version is what decides whether anyone gets the change. If you touch a skill and don’t bump the number in plugin.json and in marketplace.json (both, they have to match), the change sits written in the repo but nobody who already has the plugin installed will get it. There’s a command (claude plugin tag <path> --dry-run) that only checks that both numbers match (not that you bumped the right one); that part is still human judgment: does it break something existing (MAJOR), does it add capability (MINOR), or does it just polish what was already there (PATCH)?

What I’m taking away

None of these pieces is complicated on its own. What changes things is treating them for what they are: team infrastructure artifacts, with a version, with clear ownership, and with the discipline that a change without a new version is a change nobody receives. It’s exactly the same care we’d give an internal library (because, in practice, that’s exactly what it is).

If your team already has three people each writing their own version of “how do I explain our conventions to the AI,” it’s probably time to turn that into a plugin.


Have you built something similar? I’d love to compare notes on how you’re versioning and distributing this kind of team configuration.