{"record":{"id":"076a342a15058500","repo":"ReactiveX/rxjs","slug":"refusing-to-overwrite-a-symbolic-link-outputpat","errorCode":null,"errorMessage":"Refusing to overwrite a symbolic link: ${outputPath}","messagePattern":"Refusing to overwrite a symbolic link: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/migrate/src/node.ts","lineNumber":158,"sourceCode":"  const canonicalOutputRoot = await canonicalFutureDirectory(outputRoot, 'outputRoot');\n  const canonicalOutputs = new Set<string>();\n  for (const { sourcePath, outputPath, result } of plan.files) {\n    if (result.status === 'refused') throw new Error(`Migration result was refused for source: ${sourcePath}`);\n    const resolvedOutputPath = resolve(outputPath);\n    if (resolvedOutputPath === outputRoot) throw new Error(`Output path must identify a file below outputRoot: ${outputPath}`);\n    assertContained(outputRoot, resolvedOutputPath, `Output path is outside outputRoot: ${outputPath}`);\n    const canonicalOutputPath = await canonicalFuturePath(resolvedOutputPath);\n    assertContained(canonicalOutputRoot, canonicalOutputPath, `Output path resolves outside outputRoot: ${outputPath}`);\n    if (canonicalOutputs.has(canonicalOutputPath)) throw new Error(`Duplicate output path: ${outputPath}`);\n    canonicalOutputs.add(canonicalOutputPath);\n    await assertWritableOutput(resolvedOutputPath, options.overwrite ?? false);\n  }\n}\n\nasync function assertWritableOutput(outputPath: string, overwrite: boolean): Promise<void> {\n  try {\n    const outputStats = await lstat(outputPath);\n    if (outputStats.isSymbolicLink()) throw new Error(`Refusing to overwrite a symbolic link: ${outputPath}`);\n    if (!outputStats.isFile()) throw new Error(`Output path is not a regular file: ${outputPath}`);\n    if (!overwrite) throw new Error(`Output path already exists; enable overwrite explicitly: ${outputPath}`);\n  } catch (error: unknown) {\n    if (!isMissingPathError(error)) throw error;\n  }\n}\n\nexport async function migrateTestFiles(options: MigrateFilesOptions): Promise<readonly MigratedFile[]> {\n  if (options.write && !options.outputRoot) {\n    throw new Error('outputRoot is required when write is enabled.');\n  }\n  const plan = await planMigrationFiles(options);\n  return options.write ? applyMigrationPlan(plan, { overwrite: options.overwrite }) : plan.files;\n}\n\nfunction safeOutputPath(outputRoot: string, outputName: string): string {\n  if (!outputName || outputName === '.' || isAbsolute(outputName)) {\n    throw new Error(`Output name must be a non-empty relative path: ${outputName || '<empty>'}`);","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/ReactiveX/rxjs/blob/54796b38a57e6309f9861e174737479bb3f63f61/packages/migrate/src/node.ts#L140-L176","documentation":"assertWritableOutput uses lstat, which inspects the path itself rather than its target, and refuses to overwrite when the existing output path is a symbolic link — even with overwrite enabled. Overwriting a symlink would replace the link with a regular file and could damage setups where outputs are deliberately linked into another tree, so the migrator fail-closes.","triggerScenarios":"Calling applyMigrationPlan (or migrateTestFiles with write:true) when a planned outputPath exists and lstat reports it as a symlink. Passing overwrite:true does NOT bypass this.","commonSituations":"Output directories managed with symlinks (dotfile-style layouts, monorepo package linking, containers with linked volumes), or a previous partial migration whose outputs were symlinked elsewhere.","solutions":["Manually remove the offending symlink (rm <path>) so the migrator creates a regular file","Retarget the plan so outputPath points at the real file location instead of the link","Change your outputRoot to a directory that is not symlink-managed"],"exampleFix":"# before\napplyMigrationPlan(plan) // throws: Refusing to overwrite a symbolic link\n\n# after\nrm packages/migrated/old-link.spec.ts\napplyMigrationPlan(plan)","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises';\nasync function noSymlinkOutputs(plan: { files: { outputPath: string }[] }): Promise<boolean> {\n  for (const f of plan.files) {\n    try { if ((await lstat(f.outputPath)).isSymbolicLink()) return false; }\n    catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e; }\n  }\n  return true;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await applyMigrationPlan(plan);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Refusing to overwrite a symbolic link')) {\n    // remove the symlink manually, then retry\n  } else throw e;\n}","preventionTips":["Keep migration outputs in a directory with no symlinked entries","Remember overwrite:true does not bypass the symlink check"],"tags":["migrate","filesystem","symlink","write-safety"],"backgroundTag":"refusing-symlink-overwrite","analyzedSha":"54796b38a57e6309f9861e174737479bb3f63f61","analyzedAt":"2026-08-28T10:21:27.410Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}