google/zx · error · Fail
Can't link node_modules: ${alias} already exists
Error message
Can't link node_modules: ${alias} already exists What it means
Thrown by linkNodeModules() when the script's directory (cwd) already contains a real node_modules directory that differs from the requested symlink target. zx refuses to overwrite or destroy an existing real node_modules folder; it will only replace a missing entry or an existing symlink that points at the same target.
Source
Thrown at src/cli.ts:191
}
}
function linkNodeModules(cwd: string, external: string): string {
const nm = 'node_modules'
const alias = path.resolve(cwd, nm)
const target =
path.basename(external) === nm
? path.resolve(external)
: path.resolve(external, nm)
const aliasStat = lstat(alias)
const targetStat = lstat(target)
if (!targetStat?.isDirectory())
throw new Fail(
`Can't link node_modules: ${target} doesn't exist or is not a directory`
)
if (aliasStat?.isDirectory() && alias !== target)
throw new Fail(`Can't link node_modules: ${alias} already exists`)
if (aliasStat) return ''
fs.symlinkSync(target, alias, 'junction')
return alias
}
function lstat(p: string) {
try {
return fs.lstatSync(p)
} catch {}
}
async function readScript() {
const [firstArg] = argv._
let script = ''
let scriptPath = ''
let tempPath = ''
let argSlice = 1View on GitHub (pinned to 00a2c484e2)
Solutions
- Remove the existing node_modules in the script's directory: `rm -rf node_modules` (or `unlink node_modules` if it is a symlink).
- Run zx from a clean or temporary directory that has no node_modules.
- Drop the --prefer-local flag so zx does not attempt linking.
- Point --prefer-local at the same absolute path as the existing node_modules if you want the no-op path (alias === target returns '').
Example fix
// before: run.mjs sits in a dir that already has node_modules $ zx --prefer-local=../ext run.mjs // after: clear the conflicting dir first $ rm -rf node_modules && zx --prefer-local=../ext run.mjs
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs'
import path from 'node:path'
function isSafeToLink(cwd: string, target: string): boolean {
const alias = path.resolve(cwd, 'node_modules')
if (alias === target) return true // no-op link
const stat = fs.lstatSync(alias)
return !stat.isDirectory() || stat.isSymbolicLink() // real dir blocks linking
}
if (!isSafeToLink(scriptDir, target)) {
throw new Error(`remove existing node_modules in ${scriptDir} before --prefer-local`)
} Try / catch
try {
await runScript()
} catch (e) {
if (e instanceof Fail && /already exists/.test(e.message)) {
console.error('clean node_modules in the script dir first')
}
throw e
} Prevention
- Run zx from a clean/temp directory when using --prefer-local.
- Remove or unlink cwd/node_modules before switching prefer-local targets.
- Never assume zx will overwrite an existing real node_modules — it deliberately will not.
When it happens
Trigger: Running a script with `--prefer-local=<external>` from a directory that already has its own `node_modules` real directory whose absolute path is not equal to the external target. Switching prefer-local targets without first removing the previously created link/dir also lands here.
Common situations: Running zx from inside a project root (which already ran npm install) with -l pointing elsewhere; monorepo where the script dir and the deps dir differ; stale node_modules left over from an earlier prefer-local run.
Related errors
- Can't link node_modules: ${target} doesn't exist or is not a
- No script provided
- Failed to fetch remote script: ${remote} (${res.status})
AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13).
Data as JSON: /api/errors/32e663edc50898e4.
Report an issue: GitHub.