can1357/oh-my-pi · error · ToolError

rename target already exists: ${formatPathRelativeToCwd(newP

Error message

rename target already exists: ${formatPathRelativeToCwd(newPath, cwd)}

What it means

When applying a WorkspaceEdit containing a RenameFile operation, the destination already exists on disk. Without options.overwrite the library refuses to silently destroy the existing file; without ignoreIfExists it also won't skip, so it throws to keep the workspace consistent (the surrounding code restores any displaced files on failure).

Source

Thrown at packages/coding-agent/src/lsp/edits.ts:371

					continue;
				}
				applied.push(`Created ${formatPathRelativeToCwd(filePath, cwd)}`);
				record({ kind: "create", uri: op.uri });
			} else if (op.kind === "rename") {
				const oldPath = uriToFile(op.oldUri);
				const newPath = uriToFile(op.newUri);
				await fs.mkdir(path.dirname(newPath), { recursive: true });
				if (oldPath !== newPath) {
					// Displace an overwritten destination into a kernel-reserved sibling
					// temp dir (same filesystem, so the moves stay atomic) instead of
					// deleting it, so a failed rename (EXDEV, permissions) can restore
					// it and leave the workspace exactly as it was.
					let displaced: { dir: string; file: string } | undefined;
					try {
						const targetStat = await fs.lstat(newPath);
						if (!op.options?.overwrite) {
							if (op.options?.ignoreIfExists) continue;
							throw new ToolError(`rename target already exists: ${formatPathRelativeToCwd(newPath, cwd)}`);
						}
						// Only displace the destination when it is a distinct file. On a
						// case-insensitive filesystem a case-only rename resolves both
						// paths to the same inode; moving newPath aside would move the
						// source, so let fs.rename change the case in place instead.
						const sourceStat = await fs.lstat(oldPath);
						if (sourceStat.dev !== targetStat.dev || sourceStat.ino !== targetStat.ino) {
							const holdDir = await fs.mkdtemp(path.join(path.dirname(newPath), ".omp-displaced-"));
							const holdFile = path.join(holdDir, path.basename(newPath));
							try {
								await fs.rename(newPath, holdFile);
							} catch (error) {
								await fs.rm(holdDir, { recursive: true, force: true }).catch(() => {});
								throw error;
							}
							displaced = { dir: holdDir, file: holdFile };
						}
					} catch (error) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Check whether the target path should exist; delete or move it if the rename is intended to replace it
  2. If replacement is intended, ensure the server sets overwrite:true in the RenameFile options
  3. If skipping is intended, ensure the server sets ignoreIfExists:true

Example fix

// before
{ kind: "rename", oldUri, newUri }
// after (intentional replace)
{ kind: "rename", oldUri, newUri, options: { overwrite: true } }
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'node:fs/promises';
let exists = false;
try { await fs.lstat(newPath); exists = true; } catch {}
if (exists && !op.options?.overwrite && !op.options?.ignoreIfExists) {
  throw new Error(`target exists: ${newPath}`);
}

Try / catch

try {
  await applyWorkspaceEdit(edit, cwd);
} catch (err) {
  if (err.message.startsWith('rename target already exists:')) {
    // decide: prompt user, or re-issue edit with options: { overwrite: true }
  }
  throw err;
}

Prevention

When it happens

Trigger: A server-issued rename (e.g. file rename refactor) targets a path that already exists and the RenameFile options carry neither overwrite:true nor ignoreIfExists:true.

Common situations: Renaming a file to a name that already exists in the same directory; case-insensitive filesystems where target differs only in case; re-running an edit after a partial application.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/8147d7ecdab45fff. Report an issue: GitHub.