DEV Community

Cover image for Two things broke when I shipped my Claude Code plugin — and one of my fixes was cargo cult
Isamu Arimoto
Isamu Arimoto

Posted on

Two things broke when I shipped my Claude Code plugin — and one of my fixes was cargo cult

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
Enter fullscreen mode Exit fullscreen mode

The same file, shipped inside a plugin named mulmocast, is invoked as:

/mulmocast:story
Enter fullscreen mode Exit fullscreen mode

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:

  • locallymulmocaststory → works
  • installed from a marketplacemulmocaststoryno 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`
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 an EXDEV error.

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-…'
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
Name Defined in Used by
GitHub repo GitHub marketplace add
Marketplace name marketplace.jsonname after the @ in install
Plugin name marketplace.jsonplugins[].name, and the plugin's own plugin.jsonname 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.md into 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)