eyaltoledano/claude-task-master · error · Error

Failed to read ${filePath} for modification: ${err.message}

Error message

Failed to read ${filePath} for modification: ${err.message}

What it means

modifyJson() re-reads the target file inside the cross-process lock; if the read fails with anything other than ENOENT or a SyntaxError (i.e. a permission or I/O error), it throws 'Failed to read <path> for modification: <reason>'. The modification is intentionally aborted before applying the modifier so a broken read can never cause a lost update.

Source

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

			} catch (err: any) {
				// Distinguish between expected empty/new files and actual corruption
				if (err.code === 'ENOENT') {
					// File doesn't exist yet - start fresh
					currentData = {} as T;
				} else if (err instanceof SyntaxError) {
					// Check if it's just an empty file (our ensureFileExists writes '{}')
					const content = await fs.readFile(filePath, 'utf-8').catch(() => '');
					if (content.trim() === '' || content.trim() === '{}') {
						currentData = {} as T;
					} else {
						// Actual JSON corruption - this is a serious error
						throw new Error(
							`Corrupted JSON in ${filePath}: ${err.message}. File contains: ${content.substring(0, 100)}...`
						);
					}
				} else {
					// Other errors (permission, I/O) should be surfaced
					throw new Error(
						`Failed to read ${filePath} for modification: ${err.message}`
					);
				}
			}

			// Apply modification
			const newData = await modifier(currentData);

			// Write atomically using steno (same pattern as workflow-state-manager)
			const content = JSON.stringify(newData, null, 2);
			const writer = this.getWriter(filePath);
			await writer.write(content);
		} finally {
			if (release) {
				try {
					await release();
				} catch (err: any) {
					// Log but don't throw - lock may have been released already

View on GitHub (pinned to c0c98d367c)

Solutions

  1. Read the OS error in the message (EACCES/EISDIR/etc.) and correct it — grant read permission or remove a directory that replaced the file.
  2. Verify the path passed to the storage layer is the intended file, not a directory or symlink to an unreadable target.
  3. If in a container, ensure the volume mount is healthy and owned by the process user (match UID/GID).
  4. Retry the operation after resolving the transient I/O condition; the cross-process lock was already released.

Example fix

// before
ls -ld .taskmaster/tasks.json  # drwxr-xr-x (a directory!)
// after
rm -rf .taskmaster/tasks.json && echo '{}' > .taskmaster/tasks.json
Defensive patterns

Strategy: try-catch

Validate before calling

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

Try / catch

try {
  await fileOps.modifyJson(filePath, (data) => mutate(data));
} catch (err: any) {
  if (err.message.includes('for modification')) {
    console.error(`Fix filesystem issue on ${filePath}: ${err.message}`);
    throw err; // do not proceed — modification was aborted to avoid lost updates
  }
  throw err;
}

Prevention

When it happens

Trigger: saveTasks, createTag, deleteTag, renameTag, or any modifyJson call where fs.readFile fails with EACCES, EISDIR, EIO, EBUSY, or a similar OS-level error on the lock target file.

Common situations: Permissions changed on .taskmaster files between reads and writes (e.g. chown by another user); the file was replaced by a directory; filesystem errors in Docker/NFS mounts; antivirus or backup tools holding the file on Windows.

Related errors


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