[BUG] Plugin install fails with EXDEV when /tmp is tmpfs

Status Fixed / completed
Maintainer reply ✓ Yes — ashwin-ant
Activity 10 comments · opened Dec 20, 2025 · closed Feb 13, 2026
💡 Likely answer: A maintainer (ashwin-ant, collaborator) responded on this thread — see the highlighted reply below.

Description

Plugin installation fails with EXDEV: cross-device link not permitted when /tmp is mounted as tmpfs (common default on modern Linux distros like Ubuntu, Fedora, Arch).

Steps to Reproduce

  1. Have /tmp mounted as tmpfs (default on Ubuntu 25.10, many other distros)
  2. Have ~/.claude on a different filesystem (ext4, btrfs, etc.)
  3. Attempt to install any plugin via /plugins UI

Error Message

Error: Failed to install: EXDEV: cross-device link not permitted, 
rename '/home/user/.claude/plugins/cache/hiivmind-corpus-github' -> '/tmp/claude-plugin-temp-1766187024245'

Environment

  • OS: Ubuntu 25.10 (Questing Quetzal)
  • Kernel: 6.17.0-8-generic
  • /tmp: tmpfs mount (tmpfs on /tmp type tmpfs)
  • ~/.claude: ext4 on nvme (/dev/nvme0n1p2)
  • Claude Code Version: Latest (1.0.40)

Root Cause

Node.js fs.rename() cannot move files across filesystem boundaries. The plugin installer uses rename() to move between ~/.claude/plugins/cache/ and /tmp/, which fails with EXDEV when these are on different mount points.

This is increasingly common as:

  • Ubuntu 21.04+ defaults to tmpfs for /tmp
  • Fedora, Arch, and others also use tmpfs /tmp by default
  • Many users have /home on a separate partition

Workaround

Set TMPDIR to a location on the same filesystem as ~/.claude:

mkdir -p ~/.claude/tmp
TMPDIR=~/.claude/tmp claude

Or permanently in shell profile:

echo 'export TMPDIR="$HOME/.claude/tmp"' >> ~/.bashrc

Suggested Fix

Catch EXDEV error and fall back to copy+unlink pattern:

try {
  fs.renameSync(src, dest);
} catch (err) {
  if (err.code === 'EXDEV') {
    fs.cpSync(src, dest, { recursive: true });
    fs.rmSync(src, { recursive: true, force: true });
  } else {
    throw err;
  }
}

This is the standard pattern for handling cross-device moves in Node.js.

Related

  • #13503 - Similar rename issue (EINVAL, different root cause)

View original on GitHub ↗

10 Comments

rjmurillo · 7 months ago

Found the specific trigger. The error only happens with plugins using "source": "./.claude-plugin" in marketplace.json.

Tested on Ubuntu with tmpfs /tmp and ext4 /home:

  • worktrunk@worktrunk - FAILS - uses "source": "./.claude-plugin"
  • perplexity@perplexity-mcp-server - WORKS - uses "source": "./"
  • claude-mem@thedotmack - WORKS - no source field
  • all official plugins - WORK - no source field

So the bug is in the code path that handles subdirectory sources, not the root-level source path.

discreteds · 7 months ago

Additional findings:

1. The source field is required - it cannot be removed as a workaround.

Per the official documentation, source is a required field for plugin entries:

| Field | Type | Description |
|-------|------|-------------|
| name | string | Plugin identifier |
| source | string\|object | Where to fetch the plugin from |

Removing it results in:

Failed to load marketplace: Invalid schema: plugins.0.source: Invalid input

2. "source": "./" also triggers the bug - not just "./.claude-plugin" as originally reported.

3. Observation: On my system, /tmp is tmpfs (RAM) while ~/.claude is on disk (different filesystems). Setting TMPDIR to a path on the same filesystem as ~/.claude allowed installation to succeed.

SvenMeyer · 7 months ago

+1 - just happened to me as I have the same setup (/tmp) is tmpfs (RAM)

bendavis78 · 7 months ago

Same happened for me. This was also using a plugin that used "source": "./"

vadimcomanescu · 7 months ago

Additional Confirmation

Experiencing the same issue on Omarchy (Arch Linux fork) when installing plugins from marketplace.

Error:

Error: Failed to install: EXDEV: cross-device link not permitted, rename '/home/vadim/.claude/plugins/cache/rust-skills' -> '/tmp/claude-plugin-temp-1769078211737'

Environment:

  • OS: Omarchy (Arch Linux fork) - kernel 6.18.3-arch1-1
  • Attempting to install: rust-skills@rust-skills from marketplace
  • /tmp is on tmpfs (separate filesystem from home directory)

This confirms the issue affects the marketplace plugin installation flow as well. The TMPDIR workaround should resolve it.

dogukanatlihan · 6 months ago

Also happens on macOS, with both Claude code and cowork, when /home is on a different partition.

codemedic · 6 months ago

Root cause found in the installer source

We traced this to a specific code path in the mw() function (plugin install). The installer works in two steps:

  1. Stage: Copy plugin source to cache/<plugin.json name> (via l9H)
  2. Finalize: Rename to cache/<marketplace>/<plugin>/<version> (via Pv)

Between these steps, there's a startsWith guard:

let J = B.path.endsWith(sep) ? B.path : B.path + sep;
if (U.startsWith(J)) {
    // Final path is INSIDE staging path — circular rename
    let Q = path.join(os.tmpdir(), `claude-plugin-temp-${Date.now()}`);
    await rename(B.path, Q);   // <-- EXDEV when /tmp is tmpfs
    mkdirSync(dirname(U), { recursive: true });
    await rename(Q, U);
} else {
    await rename(B.path, U);   // Direct rename, same filesystem — works fine
}

When this triggers: If the name field in .claude-plugin/plugin.json matches the marketplace name, the staging path becomes a prefix of the final path:

  • Staging: cache/my-skills (from plugin.json name)
  • Final: cache/my-skills/my-skills/1.0.0 (from <marketplace>/<plugin>/<version>)
  • "cache/my-skills/my-skills/1.0.0".startsWith("cache/my-skills/")true/tmp path → EXDEV

Why official plugins are unaffected: The official marketplace name (claude-plugins-official) never matches any plugin name (code-review, superpowers, etc.), so the startsWith check is always false and the direct same-filesystem rename is used.

Suggested fix

Instead of using os.tmpdir() for the intermediate rename, create the temp directory within the cache root (same filesystem):

if (U.startsWith(J)) {
    let Q = path.join(cacheRoot, `plugin-temp-${Date.now()}`);  // same filesystem
    await rename(B.path, Q);
    mkdirSync(dirname(U), { recursive: true });
    await rename(Q, U);
}

Workaround for plugin authors

Use a distinct name in .claude-plugin/plugin.json that doesn't match your marketplace name (e.g., append -plugin). This avoids the startsWith condition entirely.

Environment: Claude Code v2.1.37, Linux 6.8.0, /tmp mounted as tmpfs, ~/.claude on ext4

bbtarzan12 · 6 months ago

Similar issue on WSL2: plugin install fails when CWD is a DrvFs path (/mnt/d/). Works fine from native WSL paths (~/).

Cloned repository from ... to /home/user/.claude/plugins/cache/temp_git_1770644348096_4iyku1
Failed to cache plugin: ENOENT: no such file or directory,
  rename '/home/user/.claude/plugins/cache/temp_git_...' -> '/home/user/.claude/plugins/cache/atlassian-confluence'
    at renameSync (/$bunfs/root/claude:12:1491)

Git clone succeeds, but renameSync immediately fails with ENOENT — both paths are on the same ext4 filesystem. Appears to be a Bun renameSync issue when process.cwd() is on DrvFs.

ashwin-ant collaborator · 6 months ago

Fix coming in the next version

github-actions[bot] · 6 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.