claude-code - 💡(How to fix) Fix [BUG] /plugin marketplace add fails on macOS when repo owner case ≠ marketplace.json name (unfixed since #23205) [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#52827Fetched 2026-04-25 06:19:51
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Author
Timeline (top)
labeled ×4commented ×1

/plugin marketplace add <Owner>/<repo> fails on macOS (default case-insensitive APFS) whenever the marketplace repo declares a name in marketplace.json that differs from the GitHub owner's capitalization. Same mechanism as #23205 (closed by stale bot, no fix shipped, now locked — the bot's closing message asks to file a new issue).

Claude Code version: 2.1.119 OS: macOS (APFS, case-insensitive, case-preserving — the default)

Error Message

Error: Failed to finalize marketplace cache. Please manually delete the directory at /Users/moi/.claude/plugins/marketplaces/agricidaniel-claude-ads if it exists and try again.

Technical details: ENOENT: no such file or directory, rename '/Users/moi/.claude/plugins/marketplaces/AgriciDaniel-claude-ads' -> '/Users/moi/.claude/plugins/marketplaces/agricidaniel-claude-ads'

Root Cause

Traced the code path in the shipped claude binary. The marketplace installer (zM8) uses SV5() to compute the clone directory slug:

function SV5(H) {
  return (H.source === "github"
    ? H.repo.replaceAll("/","-")          // "AgriciDaniel/claude-ads" -> "AgriciDaniel-claude-ads"
    : ...
  ).replace(/[^a-zA-Z0-9\-_]/g, "-");
}

Slug preserves the owner's casing. Flow that follows:

let A = SV5(H);
let O = path.join(K, A);   // /…/marketplaces/AgriciDaniel-claude-ads — clone destination
// (github clone happens here, into O)
// Read marketplace.json from O, get z.name = "agricidaniel-claude-ads"
let Y = path.join(K, z.name);  // /…/marketplaces/agricidaniel-claude-ads — final canonical path

if (O !== Y && !dC(H)) {           // string compare — true here
  try {
    await q.rm(Y, {recursive:true, force:true});  // <-- SELF-WIPE on case-insensitive FS
    await q.rename(O, Y);                         // <-- ENOENT: O was just wiped
  } catch (j) {
    throw Error(`Failed to finalize marketplace cache. Please manually delete ...`);
  }
}

On case-insensitive APFS, AgriciDaniel-claude-ads and agricidaniel-claude-ads resolve to the same inode. fs.rm(Y, …) therefore deletes the clone that was just created. The subsequent fs.rename(O, Y) fails with ENOENT.

The O !== Y gate only compares the path strings, so it does not detect the case-folding collision. Same bug class applies to case-insensitive NTFS on Windows.

Fix Action

Fix / Workaround

Workarounds

Code Example

/plugin marketplace add AgriciDaniel/claude-ads

---

{ "name": "agricidaniel-claude-ads", "owner": { "name": "AgriciDaniel" }, ... }

---

Error: Failed to finalize marketplace cache. Please manually delete the directory at
/Users/moi/.claude/plugins/marketplaces/agricidaniel-claude-ads if it exists and try again.

Technical details: ENOENT: no such file or directory,
rename '/Users/moi/.claude/plugins/marketplaces/AgriciDaniel-claude-ads'
    -> '/Users/moi/.claude/plugins/marketplaces/agricidaniel-claude-ads'

---

function SV5(H) {
  return (H.source === "github"
    ? H.repo.replaceAll("/","-")          // "AgriciDaniel/claude-ads" -> "AgriciDaniel-claude-ads"
    : ...
  ).replace(/[^a-zA-Z0-9\-_]/g, "-");
}

---

let A = SV5(H);
let O = path.join(K, A);   // /…/marketplaces/AgriciDaniel-claude-ads — clone destination
// (github clone happens here, into O)
// Read marketplace.json from O, get z.name = "agricidaniel-claude-ads"
let Y = path.join(K, z.name);  // /…/marketplaces/agricidaniel-claude-ads — final canonical path

if (O !== Y && !dC(H)) {           // string compare — true here
  try {
    await q.rm(Y, {recursive:true, force:true});  // <-- SELF-WIPE on case-insensitive FS
    await q.rename(O, Y);                         // <-- ENOENT: O was just wiped
  } catch (j) {
    throw Error(`Failed to finalize marketplace cache. Please manually delete ...`);
  }
}

---

/plugin marketplace add agricidaniel/claude-ads

---

git clone --depth=1 https://github.com/AgriciDaniel/claude-ads.git \
  ~/.claude/plugins/marketplaces/agricidaniel-claude-ads

---

"agricidaniel-claude-ads": {
  "source": { "source": "github", "repo": "AgriciDaniel/claude-ads" },
  "installLocation": "/Users/<you>/.claude/plugins/marketplaces/agricidaniel-claude-ads",
  "lastUpdated": "2026-04-24T00:00:00.000Z"
}
RAW_BUFFERClick to expand / collapse

Summary

/plugin marketplace add <Owner>/<repo> fails on macOS (default case-insensitive APFS) whenever the marketplace repo declares a name in marketplace.json that differs from the GitHub owner's capitalization. Same mechanism as #23205 (closed by stale bot, no fix shipped, now locked — the bot's closing message asks to file a new issue).

Claude Code version: 2.1.119 OS: macOS (APFS, case-insensitive, case-preserving — the default)

Reproducer

/plugin marketplace add AgriciDaniel/claude-ads

Repo's .claude-plugin/marketplace.json declares:

{ "name": "agricidaniel-claude-ads", "owner": { "name": "AgriciDaniel" }, ... }

Error:

Error: Failed to finalize marketplace cache. Please manually delete the directory at
/Users/moi/.claude/plugins/marketplaces/agricidaniel-claude-ads if it exists and try again.

Technical details: ENOENT: no such file or directory,
rename '/Users/moi/.claude/plugins/marketplaces/AgriciDaniel-claude-ads'
    -> '/Users/moi/.claude/plugins/marketplaces/agricidaniel-claude-ads'

The error message is misleading: there is nothing to delete — Claude Code wipes the directory itself during the failing flow. Every retry re-clones → self-wipes → re-fails.

Root cause

Traced the code path in the shipped claude binary. The marketplace installer (zM8) uses SV5() to compute the clone directory slug:

function SV5(H) {
  return (H.source === "github"
    ? H.repo.replaceAll("/","-")          // "AgriciDaniel/claude-ads" -> "AgriciDaniel-claude-ads"
    : ...
  ).replace(/[^a-zA-Z0-9\-_]/g, "-");
}

Slug preserves the owner's casing. Flow that follows:

let A = SV5(H);
let O = path.join(K, A);   // /…/marketplaces/AgriciDaniel-claude-ads — clone destination
// (github clone happens here, into O)
// Read marketplace.json from O, get z.name = "agricidaniel-claude-ads"
let Y = path.join(K, z.name);  // /…/marketplaces/agricidaniel-claude-ads — final canonical path

if (O !== Y && !dC(H)) {           // string compare — true here
  try {
    await q.rm(Y, {recursive:true, force:true});  // <-- SELF-WIPE on case-insensitive FS
    await q.rename(O, Y);                         // <-- ENOENT: O was just wiped
  } catch (j) {
    throw Error(`Failed to finalize marketplace cache. Please manually delete ...`);
  }
}

On case-insensitive APFS, AgriciDaniel-claude-ads and agricidaniel-claude-ads resolve to the same inode. fs.rm(Y, …) therefore deletes the clone that was just created. The subsequent fs.rename(O, Y) fails with ENOENT.

The O !== Y gate only compares the path strings, so it does not detect the case-folding collision. Same bug class applies to case-insensitive NTFS on Windows.

Workarounds

User-side (simplest — if you haven't run the broken add yet)

Type the owner in lowercase:

/plugin marketplace add agricidaniel/claude-ads

SV5() then produces agricidaniel-claude-ads, which matches z.name, so the rename branch is skipped entirely. First spotted in a comment on #23205.

Recovery (if you already hit the broken path and want the upstream repo casing)

Clone manually and register directly:

git clone --depth=1 https://github.com/AgriciDaniel/claude-ads.git \
  ~/.claude/plugins/marketplaces/agricidaniel-claude-ads

Then add an entry to ~/.claude/plugins/known_marketplaces.json:

"agricidaniel-claude-ads": {
  "source": { "source": "github", "repo": "AgriciDaniel/claude-ads" },
  "installLocation": "/Users/<you>/.claude/plugins/marketplaces/agricidaniel-claude-ads",
  "lastUpdated": "2026-04-24T00:00:00.000Z"
}

Restart the CC session — the "already materialized" short-circuit in the installer (for (let [Y,w] of Object.entries(K)) if (zY(w.source,q)) return …alreadyMaterialized:true) then skips the broken clone path on subsequent operations. /plugin marketplace update works fine afterwards because it git pulls the existing clone and does not re-enter the rename branch.

Proposed fixes

Either would close this (both is better):

  1. Lowercase the clone slug at construction. In SV5(), chain .toLowerCase() on the github/npm/file/directory branches. This makes O and z.name agree from the start and sidesteps the rename on every platform.

  2. Case-insensitive-safe O === Y guard. Before rm + rename, resolve both paths via fs.realpathSync (or compare fs.statSync(...).ino) and skip the branch if they collapse to the same filesystem entity. This also defends against future slug-construction changes.

Related

  • #23205 — same bug, reported Feb 2026, closed NOT_PLANNED by stale-bot, now locked. Closing bot explicitly asked reporters to file a new issue and reference the old one.
  • #14799 — EXDEV on tmpfs during plugin install (different error, same "move dir across states" code area).

extent analysis

TL;DR

To fix the issue, lowercase the clone slug at construction by modifying the SV5() function to chain .toLowerCase() on the github branch.

Guidance

  • Modify the SV5() function to lowercase the clone slug for github repositories: H.repo.replaceAll("/","-").toLowerCase().
  • As an alternative, implement a case-insensitive-safe O === Y guard by resolving both paths via fs.realpathSync or comparing fs.statSync(...).ino before attempting to rename.
  • Users can workaround the issue by typing the owner in lowercase when adding a marketplace plugin: /plugin marketplace add agricidaniel/claude-ads.
  • If the broken path has already been hit, users can recover by manually cloning the repository and registering it directly.

Example

function SV5(H) {
  return (H.source === "github"
    ? H.repo.replaceAll("/","-").toLowerCase()  // lowercase the clone slug
    : ...
  ).replace(/[^a-zA-Z0-9\-_]/g, "-");
}

Notes

The proposed fixes assume that the SV5() function is the root cause of the issue. If the issue persists after applying these fixes, further investigation may be necessary.

Recommendation

Apply the workaround by modifying the SV5() function to lowercase the clone slug, as this is a straightforward and effective solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] /plugin marketplace add fails on macOS when repo owner case ≠ marketplace.json name (unfixed since #23205) [1 comments, 2 participants]