eyaltoledano/claude-task-master · error · Error

Failed to move file from ${oldPath} to ${newPath}: ${error.m

Error message

Failed to move file from ${oldPath} to ${newPath}: ${error.message}

What it means

moveFile() uses fs.rename(oldPath, newPath) and wraps any failure into 'Failed to move file from <old> to <new>: <reason>'. rename(2) is atomic but fails across filesystem boundaries (EXDEV) and when source/destination are invalid, so the raw OS error is surfaced with both paths.

Source

Thrown at packages/tm-core/src/modules/storage/adapters/file-storage/file-operations.ts:258

	 */
	async deleteFile(filePath: string): Promise<void> {
		try {
			await fs.unlink(filePath);
		} catch (error: any) {
			if (error.code !== 'ENOENT') {
				throw new Error(`Failed to delete file ${filePath}: ${error.message}`);
			}
		}
	}

	/**
	 * Rename/move file
	 */
	async moveFile(oldPath: string, newPath: string): Promise<void> {
		try {
			await fs.rename(oldPath, newPath);
		} catch (error: any) {
			throw new Error(
				`Failed to move file from ${oldPath} to ${newPath}: ${error.message}`
			);
		}
	}

	/**
	 * Copy file
	 */
	async copyFile(srcPath: string, destPath: string): Promise<void> {
		try {
			await fs.copyFile(srcPath, destPath);
		} catch (error: any) {
			throw new Error(
				`Failed to copy file from ${srcPath} to ${destPath}: ${error.message}`
			);
		}
	}

View on GitHub (pinned to c0c98d367c)

Solutions

  1. Verify the source file exists at oldPath (ENOENT is the most common cause) and create it first if the flow expects it.
  2. If the error is EXDEV (cross-device link), copy then delete instead of rename: use copyFile followed by deleteFile.
  3. Ensure the destination directory exists and is writable (mkdir -p + permissions) before moving.
  4. Check for the destination being an existing non-empty directory and choose a different destination path or remove it.

Example fix

// before
await fileOps.moveFile('/mnt/vol1/tasks.json', '/mnt/vol2/tasks.json'); // EXDEV
// after
await fileOps.copyFile('/mnt/vol1/tasks.json', '/mnt/vol2/tasks.json');
await fileOps.deleteFile('/mnt/vol1/tasks.json');
Defensive patterns

Strategy: fallback

Validate before calling

import { stat, access, constants } from 'fs/promises';
import path from 'path';
export async function canMoveFile(oldPath: string, newPath: string): Promise<boolean> {
  try {
    const s = await stat(oldPath);
    if (!s.isFile()) return false;
    await access(path.dirname(newPath), constants.W_OK);
    return true;
  } catch { return false; }
}

Try / catch

try {
  await fileOps.moveFile(oldPath, newPath);
} catch (err: any) {
  if (err.message.includes('EXDEV') || err.message.includes('cross-device')) {
    // fallback: copy + delete across filesystem boundaries
    await fileOps.copyFile(oldPath, newPath);
    await fileOps.deleteFile(oldPath);
    return;
  }
  if (err.message.startsWith('Failed to move file')) {
    console.error(`Move failed: verify ${oldPath} exists and ${path.dirname(newPath)} is writable (${err.message})`);
  }
  throw err;
}

Prevention

When it happens

Trigger: moveFile invoked when the source does not exist (ENOENT), source or destination directory lacks permission (EACCES), old and new paths are on different devices/mounts (EXDEV), or newPath is a non-empty directory (ENOTEMPTY/EEXIST).

Common situations: Renaming a tag file where .taskmaster spans a Docker volume boundary (rename across mounts fails with EXDEV); the source file was never created; destination directory missing or unwritable; Windows file locking by another process.

Related errors


AI-assisted analysis of eyaltoledano/claude-task-master@c0c98d367c (2026-08-29). Data as JSON: /api/errors/4c827e8506e6a7ed. Report an issue: GitHub.