Kleines qupaya-Logo Großes qupaya-Logo
  • Blog
  • Case Study
  • Über
  • Erstgespräch buchen
Erstgespräch buchen
    • Guided Program Das Programm im Überblick
    • Guided AI Euer dauerhafter Engineering-Guide
    • Guided Angular Spezialisiert für Angular-Teams
    • Beratung
    • Software Engineering
    • AngularJS Migration
    • Von Planung bis Umsetzung
    • Alle Services →
  • Blog
  • Case Study
  • Über
  • Guided Program Das Programm im Überblick
  • Guided AI Euer dauerhafter Engineering-Guide
  • Guided Angular Spezialisiert für Angular-Teams

Weitere Services

  • Beratung
  • Software Engineering
  • AngularJS Migration
  • Von Planung bis Umsetzung
  • Alle Services →

Blog

Share Claude Code skills across repos with a local plugin marketplace

How to give every repo inside one work folder the same skills, agents, hooks and MCP servers, without making them global and without copying them into each project.

Marco Salas Díaz
· 13. November 2026 · 20 Min. Lesezeit Nur auf Englisch verfügbar
KI-generiert

Tested on Claude Code 2.1.285, macOS, zsh, November 2026

Put a .claude/ folder in the folder that holds all of one customer’s repos, and you’d expect every repo inside to pick it up, the way they pick up CLAUDE.md. They don’t. Claude Code reads a parent folder’s CLAUDE.md and .mcp.json, and ignores its skills, agents, commands and hooks.

What works is a local plugin marketplace: one folder on your disk, registered once per machine and installed per project with one command. This post shows that setup and the tests behind it.

It’s for people who already use Claude Code skills, agents, hooks or MCP servers and keep several repos per customer, team or side business. You don’t need to know plugins or marketplaces yet. I explain both when they come up.

The problem

If you work for several customers, each one usually has more than one repo, and each one has its own rules: a coding standard, a review checklist, a design system, a required way to write commits. Those rules make good Claude Code skills. They also differ between customers, and sometimes contradict each other. What customer X requires, customer Y may forbid.

So the skills for customer X should apply to every customer X project and to nothing else on the machine. Putting them in ~/.claude/ is too wide, because they’d reach every customer. Copying them into each repo means keeping copies in sync.

Too wide is more than untidy. Tooling that is correct for one customer can be risky for another:

Kind of risk Right for customer X In customer Y’s repo
Quality A design skill that pushes for a bold, distinctive look. It argues against Y’s strict design system, and the output fails review.
Process A release skill that commits to main and tags, because X deploys from tags. Y requires reviewed pull requests. The agent follows X’s steps and bypasses them.
Data An MCP server connected to X’s issue tracker, with X’s credentials. The agent can file a ticket about Y’s code in X’s tracker. That’s a confidentiality breach.

Two customers’ repos on one machine: a shared plugin covers only customer X’s projects, while customer Y’s stay outside it. A crossed-out dome over the whole machine marks the global setup as too wide.

One customer’s tooling, one customer’s repos. (AI Generated)

The first is an annoyance, the second breaks a customer’s rules, and the third can break a contract. None of them needs anyone to do something wrong. The tooling only has to be present in the wrong session.

In this post, all of customer X’s projects live in one folder, ~/dev/customer-x/, each as its own git repo. The same setup fits other groupings too:

  • A company wants a default set of skills for all of its own projects.
  • You use one machine for work projects and personal projects, and want to keep the two apart.
  • Anything else where a group of repos should share tooling that the rest of the machine doesn’t get.

This post may expire. There’s a feature request asking for skills, agents and commands to walk up the folder tree like CLAUDE.md does. If it ships, a parent .claude/ folder will do most of this and you won’t need what follows. As of November 2026 it’s still open: filed on 18 February 2026, labelled as an enhancement, with 50 thumbs-up and 17 comments, the latest from September.

The comments there collect the workarounds people use today: symlinks from each project, installing every skill globally into ~/.claude/skills/, a custom MCP server that gathers the files, and listing folders under additionalDirectories. The setup in this post has a few advantages over those:

  • It’s scoped. Only the projects where you install the plugin get it. A global install reaches every repo on the machine, including other customers’ repos.
  • It’s one command per project. No set of links to create and repair in every repo and every fresh clone.
  • It covers more than skills. Agents, hooks and MCP servers travel in the same plugin.
  • It’s a built-in, documented feature. There’s no custom server or install script to maintain.

What actually inherits from a parent folder

I tested this with a throwaway parent folder holding one of everything, and a git repo inside it. I started Claude Code in the repo and looked at what the session had loaded.

In the parent folder Loaded in the project?
CLAUDE.md ✅ Yes. Read from the start folder and every folder above it.
.mcp.json ✅ Yes. The parent’s server connected in the project. I expected a no here.
.claude/skills/ ❌ No. The search stops at the git repo root. (Skills docs)
.claude/agents/ ❌ No.
.claude/commands/ ❌ No.
Hooks in .claude/settings.json ❌ No. Hooks are registered in a settings file, and a hook registered there never ran. (Settings docs)

Each row was checked in a headless session (claude -p). As a control, the same agent, MCP server and hook all loaded when placed in the project itself. An interactive session may ask you to approve a project MCP server first.

Here is how a check looks, for the first three rows. The parent folder has a CLAUDE.md containing a marker word, a .mcp.json with a server called parent-mcp, and a skill called parent-skill. With --output-format stream-json --verbose, the first line of output is an init event that lists what the session loaded.

$ cd ~/dev/customer-x/super-project-A
$ claude -p --output-format stream-json --verbose \
    "What is the parent CLAUDE.md marker? Answer in one line." < /dev/null

# from the init event, trimmed to the parent folder's entries
"mcp_servers": [{ "name": "parent-mcp", "status": "connected", "source": "project" }, …]
"skills": [ … ]   # parent-skill is not in the list

# from the result event
"result": "ORANGE-17."

Skills are the subtle case. The docs say they’re loaded from the start folder and every parent up to the repository root. That’s built for monorepos. With one repo per project, the search ends at the project folder:

  1. customer-x/super-project-A/.claude/skills/: Start folder, and the repo root. Loaded.
  2. customer-x/.claude/skills/: Above the repo root. Never read.

A parent folder holds instructions, an MCP server, skills, agents and hooks. At the git repo root, only the instructions and the MCP server pass through to the project; skills, agents and hooks are stopped.

Instructions and MCP servers get through. Skills, agents and hooks don’t. (AI Generated)

So a parent folder already covers instructions (CLAUDE.md) and MCP servers (.mcp.json). Skills, agents, commands and hooks need another route.

The fix: a local plugin marketplace

A plugin is a folder that bundles skills, agents, hooks and MCP servers. A marketplace is just a catalogue file listing plugins, and it can live in a folder on your own disk. Nothing is published or uploaded. You register the folder once per machine, then install the plugin per project with --scope project or --scope local, so it only appears where you asked for it.

Everything lives in one folder next to the projects. It’s self-contained, so it can be copied to another work folder later.

customer-x/
├── claude-shared/                       ← the marketplace
│   ├── README.md
│   ├── .claude-plugin/marketplace.json   ← catalogue: its name + list of plugins
│   ├── plugins/
│   │   └── shared/                      ← the plugin
│   │       ├── .claude-plugin/plugin.json ← name + version
│   │       ├── skills/<name>/SKILL.md
│   │       ├── agents/<name>.md
│   │       ├── hooks/hooks.json
│   │       ├── .mcp.json
│   │       └── mcp/                      ← bundled MCP server code
│   └── templates/
│       └── settings.json                 ← copied into new projects, never loaded
├── super-project-A/
└── mega-project-X/

The two manifests are short. All fields are described in the marketplace docs and the plugins reference.

.claude-plugin/marketplace.json

{
  "name": "customer-x",
  "owner": { "name": "your-name" },
  "plugins": [
    { "name": "shared", "source": "./plugins/shared" }
  ]
}

plugins/shared/.claude-plugin/plugin.json

{
  "name": "shared",
  "description": "Shared skills, agents and hooks for customer X projects",
  "version": "0.1.0"
}

Inside the plugin, each piece is a plain file. These are the smallest versions I tested: a skill, an agent, a hook that adds a line to Claude’s context on every prompt, and the MCP server entry. Paths inside the plugin use ${CLAUDE_PLUGIN_ROOT}, so they work wherever the plugin is installed.

plugins/shared/skills/hello/SKILL.md

---
name: hello
description: Replies with a fixed greeting. Use when the user asks to test the shared plugin.
---

Reply with exactly: "Hello from the shared plugin."

plugins/shared/agents/hello.md

---
name: hello
description: Replies with a fixed greeting. Use when the user asks to test the shared plugin's agent.
---

Reply with exactly: "Hello from the shared agent."

The hook calls a script in the plugin. A script is read each time it runs, so you can edit it without reloading anything.

plugins/shared/hooks/hooks.json

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/hello.sh" }
        ]
      }
    ]
  }
}

plugins/shared/hooks/hello.sh

#!/bin/sh
# Runs on every prompt. Its output is added to Claude's context.
echo 'Shared plugin hook ran.'

plugins/shared/.mcp.json

{
  "mcpServers": {
    "hello": {
      "command": "node",
      "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/hello.mjs"]
    }
  }
}

The server itself is a small Node script with no dependencies that speaks JSON-RPC over stdin and stdout, following the Model Context Protocol:

plugins/shared/mcp/hello.mjs

// Minimal MCP server: JSON-RPC over stdio, one tool, no dependencies.
import { createInterface } from 'node:readline';

const send = (msg) => process.stdout.write(JSON.stringify(msg) + '\n');
const tool = {
  name: 'greet',
  description: 'Returns a fixed greeting.',
  inputSchema: { type: 'object', properties: {} },
};

createInterface({ input: process.stdin }).on('line', (line) => {
  const { id, method, params } = JSON.parse(line);
  if (id === undefined) return; // notification, no reply
  if (method === 'initialize') {
    send({ jsonrpc: '2.0', id, result: {
      protocolVersion: params.protocolVersion,
      capabilities: { tools: {} },
      serverInfo: { name: 'hello', version: '0.1.0' },
    } });
  } else if (method === 'tools/list') {
    send({ jsonrpc: '2.0', id, result: { tools: [tool] } });
  } else if (method === 'tools/call') {
    send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: 'Hello from the shared MCP server.' }] } });
  } else {
    send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
  }
});

All of these files are in an example repo. Clone it to follow along.

Wiring it up

Two commands: one per machine, one per project (installing plugins).

# once per machine: remember where the marketplace lives
$ claude plugin marketplace add ~/dev/customer-x/claude-shared
✔ Successfully added marketplace: customer-x

# once per project, from inside it
$ claude plugin install shared@customer-x --scope project
✔ Successfully installed plugin: shared@customer-x (scope: project)

The first command records the marketplace’s folder in two places on your machine: ~/.claude/plugins/known_marketplaces.json, and an extraKnownMarketplaces entry in ~/.claude/settings.json. The second records the install for that project’s path in ~/.claude/plugins/installed_plugins.json, and writes one line to the project’s .claude/settings.json:

.claude/settings.json

{ "enabledPlugins": { "shared@customer-x": true } }

When a session starts, Claude Code reads that line, looks up the customer-x marketplace among the ones registered on your machine, finds the folder, and loads the plugin. A project without that line never sees it.

One shared plugin folder is cabled to two projects whose switches are on, and they receive its skills, agents and hooks. A third project is left unplugged with its switch off and gets nothing.

Plugged in per project. (AI Generated)

There are two scopes, and I use both. --scope project writes to .claude/settings.json, which is committed, so the repo records that it uses the plugin. --scope local writes to the git-ignored .claude/settings.local.json, which keeps the plugin to your own copy of the repo. Sessions behave the same either way.

Committing the line doesn’t give teammates the plugin. The marketplace is registered on your machine only. I tested a project whose settings enable the plugin, on a setup where customer-x was never added: the plugin didn’t load, and the headless session showed no warning. I didn’t check what an interactive session shows. If you’re the only one using the marketplace, --scope local keeps that line out of the shared repo.

Edits are live, with no version bump

I first believed the opposite. Installing a plugin creates a copy under ~/.claude/plugins/cache/, so I assumed sessions ran from that copy and that every edit needed a version bump and two update commands. An earlier draft of this post said so.

It isn’t true for this setup. When the marketplace was added from a local folder and the plugin’s source is a relative path, Claude Code loads the plugin in place from the marketplace folder. I checked it with one of each kind: a skill, an agent, a hook and an MCP server. I edited all four in the shared folder, left the version alone, and started a new session in a project. All four edits were there, and the session reported the plugin’s path as the shared folder, not the cache.

To pick up an edit What it covers
Start a new session Everything.
Run /reload-plugins Skills and agents, inside the running session.
Nothing A hook script’s contents. The script is read each time it runs.

One gap: after /reload-plugins, an MCP server that was already running kept its old code. MCP changes needed a new session. The version bump only matters for marketplaces hosted elsewhere, such as a git repo, where users do get a cached copy.

Other things that surprised me

--plugin-dir tries a plugin without installing it

--plugin-dir loads a plugin straight from a folder, for that session only:

$ claude --plugin-dir ~/dev/customer-x/claude-shared/plugins/shared

I tested it with a one-line skill. The session loaded it from the folder, an edit showed up in the next session, nothing was installed, and a session without the flag didn’t know the skill. It’s handy for trying the plugin in a project you don’t want to install it in.

Everything gets a namespace

Plugin content is prefixed with the plugin’s name. A skill called frontend-design becomes /shared:frontend-design, and MCP tools get the longest names of all.

The install step isn’t optional

I keep an optional templates/settings.json to copy into new projects, and it already enables the plugin. Copying it isn’t enough. The install command is what records the plugin as installed for that project on this machine, in installed_plugins.json, so you still run it. The reverse does work: the install command alone creates the settings file, so the template is a convenience and not a step.

Keep the template out of the plugin folder. A plugin can carry a settings.json of its own, so a template placed there stops being inert.

Settings don’t travel

A plugin can’t ship your permissions or settings.local.json. Those stay per project or go in ~/.claude/settings.json.

Testing every piece

Before putting anything real in the plugin, I added one throwaway mock of each kind, like the files above, and called each from a fresh session inside a project:

Piece Called as Result
Skill /shared:<skill> Replied with its greeting
Agent shared:<agent> Replied with its greeting
Hook UserPromptSubmit Its output reached Claude’s context
MCP mcp__plugin_shared_<server>__<tool> Connected; the tool answered

As a control, I called the same skill from a sibling project without the install. It wasn’t found, which is the point.

Why not symlinks, --add-dir or a separate config folder?

I tried the other routes before settling on the plugin. Each works for part of the job.

Symlinks

Entries in a project’s .claude/ can be symlinks to files elsewhere (one write-up of the approach). I linked a skill folder, an agent file, .mcp.json and .claude/settings.local.json (carrying a hook) to a shared folder, and all four loaded. Replacing the whole .claude/skills folder with a single link also worked: every skill behind it loaded.

The cost is upkeep. You create the links in every project and again in every fresh clone, and a project whose skills folder is a link can’t keep skills of its own next to the shared ones. I tested on macOS; symlinks behave differently on Windows.

--add-dir

Starting Claude with claude --add-dir ~/dev/customer-x loads that folder’s skills, commands and agents. In my test it didn’t load the folder’s .mcp.json, its settings or its hooks. You need the flag every session, and it also opens file access to the whole folder. The similarly named permissions.additionalDirectories setting only grants file access and loads nothing.

Symlink Local plugin --add-dir
Skills ✅ Yes ✅ Yes ✅ Yes
Agents ✅ Yes ✅ Yes ✅ Yes
MCP servers ✅ Yes ✅ Yes ❌ No
Hooks ✅ Yes, in a linked settings file ✅ Yes ❌ No
Settings ✅ Yes ❌ No ❌ No

Every cell is the result of my own test on Claude Code 2.1.285, not a reading of the docs. “Settings: No” for the plugin means a hook I put in the plugin’s own settings.json didn’t run; hooks belong in the plugin’s hooks/hooks.json.

Symlinks cover the most ground, so why the plugin? One install command per project replaces a set of links, the project’s own .claude/ stays free for project things, and the install is recorded in a settings file you can commit or keep local.

A separate config folder per customer

There’s a route of a different kind. Claude Code reads its user-level folder from the CLAUDE_CONFIG_DIR environment variable, so a terminal profile (in iTerm2, for example) can start with CLAUDE_CONFIG_DIR=~/.claude-customer-x and get a whole separate Claude Code for that customer.

I tried it with an empty folder holding one skill. The skill loaded, and nothing from my normal setup came along: none of my own skills, MCP servers or plugins, and the session wasn’t logged in.

Config folder per customer Local plugin per customer
What is separated Everything: skills, settings, permissions, MCP servers, history, login. Only what’s in the plugin. Your own global setup stays available.
What decides it How you start Claude Code. Which project you’re in.
Setup One profile per customer, plus a login and your personal tooling copied into each. One marketplace per customer, one install command per project.
Sharing with a team Each person builds their own profile. The repo’s settings can record the plugin, but each person still registers the marketplace.

The profile is the stronger wall. If a customer requires its own account, or nothing from one customer may sit next to another’s, use it. Its weak point is that it depends on the launch: open the same repo from an editor extension or a plain terminal tab and you get your default setup, with no warning.

For the job in this post, sharing a set of skills across one customer’s repos, the plugin fits better. It follows the project instead of the terminal, and it doesn’t make you rebuild your own setup once per customer. The two also combine: a customer profile for the hard separation, and a plugin inside it for the shared skills.

A real skill in the plugin

The first real thing I put in the plugin is Anthropic’s frontend-design skill, copied unchanged with its licence. It’s a good example of a skill you want in some projects only: useful where there’s no design system, and unwelcome in repos whose customer already has a strict one. In a project with its own palette and fonts, it kept the project’s tokens instead of fighting them, which was my worry. It loaded by itself for a request to restyle a section, and didn’t for a request to propose a direction. That’s one prompt per case, so if you want a skill for certain, call it by name.

Reusing it elsewhere

Because everything is in one folder, setting up another work folder is a copy plus a rename:

  1. Copy claude-shared/ into the new work folder.
  2. Change the marketplace name in marketplace.json, since two marketplaces on one machine can’t share a name.
  3. Register it and install it per project with the new names.

In short

  1. A parent folder’s CLAUDE.md and .mcp.json reach the projects inside it. Skills stop at the git root, and nothing in the parent’s .claude/ is read.
  2. A local plugin marketplace shares skills, agents, hooks and MCP servers across the projects you choose, with one install command each.
  3. Register the marketplace once per machine; install with --scope local for yourself, or --scope project to record it in the repo. Teammates still need the marketplace on their machine.
  4. A plugin from a local-folder marketplace loads in place: edit a file, then start a new session or run /reload-plugins. No version bump.

One more idea: does the folder have to be local?

Everything above keeps claude-shared/ on one disk, for one person. But it’s only a folder with two manifests in it, and a marketplace can also be a git repository.

That opens a larger version of the same idea: one skills repo per customer. Whoever has access to the repo gets that customer’s skills, and whoever doesn’t, doesn’t. A project’s settings can name the repo, so a new teammate who clones the project is offered the plugin without setting anything up.

It isn’t free. According to the docs, a marketplace added from a repo is copied into a cache, so the live edits I liked would be gone for the people who consume it that way. Maybe the person who writes the skills keeps a local clone and everyone else uses the repo.

I haven’t tested any of this yet. One folder for one person works today. What it looks like as one repo for a whole team is the next thing I want to try.

How this post was made. I set this up in a working session with Claude Code, for my own projects, not for this post. I wrote the post later in a separate Claude Code session, using what that work had shown. I reviewed and corrected the text.

Further reading

  • Example repo with the files from this post
  • Claude Code docs: plugin marketplaces
  • Claude Code docs: plugins
  • Claude Code docs: skills
  • Claude Code docs: plugins reference
  • Claude Code docs: CLAUDE.md and memory
  • Xebia: the DRY principle with Claude Code plugins
  • Feature request: let skills, agents and commands walk up parent folders
Großes qupaya-Logo

Unternehmen

  • Über uns
  • Case Study
  • Karriere
  • Kontakt

Services

  • Guided Program
  • Guided AI
  • Guided Angular
  • Consulting
  • Software Engineering
  • Von Planung bis Umsetzung
  • AngularJS Migration
  • Angular Health Check

Rechtliches

  • Impressum
  • Datenschutz
  • Barrierefreiheit

Sprachen

  • Englisch
  • Deutsch

Copyright © 2026 qupaya. Alle Rechte vorbehalten.