[BUG] Plugin install fails with EXDEV when /tmp is tmpfs
Status Fixed / completed
Maintainer reply ✓ Yes — ashwin-ant
Workaround ✓ Mentioned in thread ↓
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
- Have
/tmpmounted as tmpfs (default on Ubuntu 25.10, many other distros) - Have
~/.claudeon a different filesystem (ext4, btrfs, etc.) - Attempt to install any plugin via
/pluginsUI
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)
10 Comments
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:
"source": "./.claude-plugin""source": "./"So the bug is in the code path that handles subdirectory sources, not the root-level source path.
Additional findings:
1. The
sourcefield is required - it cannot be removed as a workaround.Per the official documentation,
sourceis 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:
2.
"source": "./"also triggers the bug - not just"./.claude-plugin"as originally reported.3. Observation: On my system,
/tmpis tmpfs (RAM) while~/.claudeis on disk (different filesystems). SettingTMPDIRto a path on the same filesystem as~/.claudeallowed installation to succeed.+1 - just happened to me as I have the same setup (
/tmp) is tmpfs (RAM)Same happened for me. This was also using a plugin that used
"source": "./"Additional Confirmation
Experiencing the same issue on Omarchy (Arch Linux fork) when installing plugins from marketplace.
Error:
Environment:
rust-skills@rust-skillsfrom marketplace/tmpis on tmpfs (separate filesystem from home directory)This confirms the issue affects the marketplace plugin installation flow as well. The
TMPDIRworkaround should resolve it.Also happens on macOS, with both Claude code and cowork, when /home is on a different partition.
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:cache/<plugin.json name>(vial9H)cache/<marketplace>/<plugin>/<version>(viaPv)Between these steps, there's a
startsWithguard:When this triggers: If the
namefield in.claude-plugin/plugin.jsonmatches the marketplace name, the staging path becomes a prefix of the final path:cache/my-skills(from plugin.jsonname)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 →/tmppath → EXDEVWhy official plugins are unaffected: The official marketplace name (
claude-plugins-official) never matches any plugin name (code-review,superpowers, etc.), so thestartsWithcheck 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):Workaround for plugin authors
Use a distinct
namein.claude-plugin/plugin.jsonthat doesn't match your marketplace name (e.g., append-plugin). This avoids thestartsWithcondition entirely.Environment: Claude Code v2.1.37, Linux 6.8.0,
/tmpmounted as tmpfs,~/.claudeon ext4Similar issue on WSL2: plugin install fails when CWD is a DrvFs path (
/mnt/d/). Works fine from native WSL paths (~/).Git clone succeeds, but
renameSyncimmediately fails with ENOENT — both paths are on the same ext4 filesystem. Appears to be a BunrenameSyncissue whenprocess.cwd()is on DrvFs.Fix coming in the next version
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.