My skills worked. I'd been using them locally for weeks. Then I packaged them as a plugin, installed it the way a stranger would, and hit things that don't exist locally — and that don't produce an error message pointing at the cause.
Two of them were real. The third was a rule I'd been repeating for months, which turned out to be about something else entirely. Writing this post is what made me check it.
1. Your skill's name changes, and skill-to-skill calls break silently
A skill in ~/.claude/skills/story/SKILL.md is invoked as:
/story
The same file, shipped inside a plugin named mulmocast, is invoked as:
/mulmocast:story
The plugin name becomes a prefix. Which is fine, until you remember that skills can call other skills.
My mulmocast skill is a router: it reads what you asked for and hands off to story, narrate, illustrate and so on. Written the obvious way, that hand-off says story. So:
-
locally —
mulmocast→story→ works -
installed from a marketplace —
mulmocast→story→ no such skill
I never saw an error that named the cause. The hand-off simply didn't happen, and the skill carried on as though that step had been optional.
The fix is to write both names, qualified one first:
1. Try `mulmocast:story` → if not found, try `story`
2. Try `mulmocast:narrate` → if not found, try `narrate`
One note on notation: a user types /story to invoke a skill. Inside a SKILL.md you are naming a skill for the model to dispatch to, not typing a slash command — so write it without the slash, as above.
2. Your CLI's name changes too
Same shape of problem, different layer.
My skills drive a CLI that ships as its own npm package. The plugin repo carries the skills, references and example scripts — not the CLI. While developing the CLI, I ran it from inside its repo:
yarn run cli images script.json
That line is fine in a terminal I own and wrong in a SKILL.md, because the person reading it installed a plugin. They don't have that checkout, so yarn run cli resolves to nothing.
The obvious fix is to write the published bin name instead:
{
"name": "mulmocast",
"bin": {
"mulmo": "lib/cli/bin.js",
"mulmocast": "lib/cli/bin.js"
}
}
That works — if they installed the package globally. Which they may not have, and which you have no way to check from inside a skill.
So what my skills actually say is this:
npx mulmocast@latest movie script.json
npx resolves the package and downloads a temporary copy when what's installed doesn't satisfy the spec. No install step to document, and no version to keep in sync with your prose. If you keep a yarn run cli line at all, mark it as the one for people working in the repo — it is not a fallback for your users, it's a note to yourself.
Both of these have the same root cause: you are writing instructions from inside an environment your reader is not in. Every name you put in a SKILL.md — a skill, a command, a path — gets resolved somewhere else.
3. The one I was wrong about
Here is advice I had been giving, including in writing:
Never give your marketplace and your plugin the same
name. If you do, installation fails on Linux with anEXDEVerror.
I believed it. I set my repo up that way. I couldn't tell you where I first picked it up.
I don't use Linux. I never hit this, never reproduced it, never checked it. Writing this post is what finally sent me looking for the source — anthropics/claude-code#14799.
It has nothing to do with names.
Error: Failed to install: EXDEV: cross-device link not permitted,
rename '/home/user/.claude/plugins/cache/…' -> '/tmp/claude-plugin-temp-…'
On most current Linux distributions /tmp is tmpfs, while ~/.claude sits on your real disk. Two filesystems. fs.rename() can't move a file across that boundary, and the installer was renaming between them.
So it fired for any plugin, whatever anything was called. The workaround was to put the temp directory on the same filesystem:
export TMPDIR="$HOME/.claude/tmp"
And it's fixed — the issue closed in February. On a current Claude Code there's nothing here for you to do.
Why a wrong rule survived that long
This is the part I want to keep.
The rule was harmless. Giving the marketplace and the plugin different names costs nothing, breaks nothing, and looks tidy. So I followed it, everything worked, and nothing ever contradicted me.
A rule that's expensive gets challenged — sooner or later someone asks whether it's worth it. A rule that's free just accumulates. I'd been passing on a diagnosis I had never once tested, and the only reason I found out is that I sat down to write it as fact.
For the record, here are the names in a working setup — and note that two of them are supposed to match:
// .claude-plugin/marketplace.json
{
"name": "mulmocast-plugins", // the marketplace
"plugins": [
{ "name": "mulmocast", ... } // ← must MATCH plugin.json
]
}
// .claude-plugin/plugin.json
{ "name": "mulmocast" }
While we're here: there are three names, not one
The reason the above is confusing at all is that a plugin involves three separate names, and the docs use them in different places:
receptron/mulmocast-claude-plugin ← GitHub repo
↓
marketplace.json "name": "mulmocast-plugins" ← marketplace name
marketplace.json plugins[0].name: "mulmocast" ← plugin name
↓
install: mulmocast@mulmocast-plugins
^^^^^^^^^ ^^^^^^^^^^^^^^^^
plugin marketplace
| Name | Defined in | Used by |
|---|---|---|
| GitHub repo | GitHub | marketplace add |
| Marketplace name |
marketplace.json → name
|
after the @ in install
|
| Plugin name |
marketplace.json → plugins[].name, and the plugin's own plugin.json → name
|
before the @ in install
|
The repo name is used once in this sequence, when registering the marketplace. After that you work from the names inside the JSON:
claude plugin marketplace add receptron/mulmocast-claude-plugin # repo name
claude plugin install mulmocast@mulmocast-plugins # plugin@marketplace
Minimum layout. These can be two repos or one — mine is one:
my-plugin-repo/
.claude-plugin/
marketplace.json # the catalogue
plugin.json # this plugin's metadata
skills/
my-skill/
SKILL.md # the actual skill
Which distribution method to pick
There are three, and they're not competing for the same job.
| Reach | Ships MCP servers | Ships hooks | Install | |
|---|---|---|---|---|
| Your own marketplace | Claude Code | ✅ | ✅ | /plugin install name@market |
| Official store (claude-plugins-official) | Claude Code, no setup step | ✅ | ✅ | /plugin install name@claude-plugins-official |
| Skills CLI | Cursor, Codex, OpenCode and dozens more | ❌ | ❌ | npx skills add owner/repo |
Roughly:
- Claude Code only, and you use MCP servers or hooks → plugin. Start with your own marketplace, apply to the official store once it's proven.
-
You want it to work in Cursor, Codex and the rest too → Skills CLI. It symlinks
SKILL.mdinto each agent's directory, so one repo covers all of them. You give up MCP and hooks.
The columns say ships deliberately. Skills CLI distributes SKILL.md files; it does not install plugin manifests, so MCP servers and plugin hooks don't come with them. The target agent may well support hooks on its own.
And the thing I'd actually weigh about the official store isn't discovery — it's that users skip marketplace add entirely. One less step where someone gives up.
What the three have in common
The first two live in the same place: I had only ever run this where the names happen to be right. Locally the skill is story and the CLI is yarn run cli. Published, it's mulmocast:story and mulmocast. Both failures sit in that gap.
The third is the same gap from the other side. I'm on macOS, so a Linux-only failure was never going to reach me — and because my workaround for it was free, no evidence was ever going to reach me either. I'd built a belief that couldn't be falsified from where I was standing.
So the useful habit isn't "test the plugin" — it's install it the way a stranger would, on a machine that isn't yours, before you tell anyone it exists.
And if you're carrying a rule you've never seen fail: that isn't evidence it works. It might just be cheap.
Docs: Claude Code plugins · Marketplaces · Agent Skills spec
Top comments (0)