affaan-m/ECC · error

Refusing to read non-file path

Error message

Refusing to read non-file path: ${filePath}

What it means

readFileWithMetadataNoFollow opens files with O_NOFOLLOW and then double-checks with fstat that the opened descriptor is a regular file before reading. If the path resolved to something else (symlink, directory, FIFO, device), the library refuses to read it rather than following or misinterpreting it. This prevents TOCTOU symlink attacks when reading files named by untrusted install state.

Solutions

  1. Point the operation at a regular file path, not a directory, symlink, or special file.
  2. If the target is a symlink to a real file, use the real file's path (or replace the symlink with a copy) since O_NOFOLLOW refuses to follow links.
  3. Re-run the operation if it was a transient race; investigate what replaced the file if it recurs.
  4. Verify the path with `ls -la` / `file` and ensure it is a plain regular file before retrying.

Example fix

// before: state says destinationPath: ~/.claude/settings.json, but that is a directory
// after
rm -rf ~/.claude/settings.json   # remove the directory
echo '{}' > ~/.claude/settings.json  # real file, then retry install
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function assertRegularFileNoSymlink(p) {
  const st = fs.lstatSync(p); // lstat: does not follow symlinks
  if (!st.isFile()) throw new Error(`Not a regular file: ${p}`);
}

Type guard

function isPlainRegularFile(p) {
  try { const st = fs.lstatSync(p); return st.isFile() && !st.isSymbolicLink(); } catch { return false; }
}

Try / catch

try {
  await install(operations);
} catch (err) {
  if (/Refusing to read non-file path/.test(err.message)) {
    console.error(`Check ${err.message.split(': ')[1]} — replace symlink/dir with a real file.`);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling the read helper with a path that is not a regular file at open time: a directory path, a symlink (O_NOFOLLOW makes open fail or the check fails), a FIFO/socket/device node, or a path swapped between the existsSync check and the read.

Common situations: install-state recording a destinationPath that is actually a directory; users symlinking managed files elsewhere; a race where another process replaces the file with a symlink or special file while install runs.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/78a9de293ba2c0ad. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install-lifecycle.js:520

    );
    fs.ftruncateSync(fileDescriptor, 0);
    fs.writeFileSync(fileDescriptor, content);
    if (mode !== undefined) {
      fs.fchmodSync(fileDescriptor, mode);
    }
  } finally {
    fs.closeSync(fileDescriptor);
  }
}

function readFileWithMetadataNoFollow(filePath, encoding) {
  const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
  const fileDescriptor = fs.openSync(filePath, flags);

  try {
    const stat = fs.fstatSync(fileDescriptor);
    if (!stat.isFile()) {
      throw new Error(`Refusing to read non-file path: ${filePath}`);
    }
    return {
      content: fs.readFileSync(fileDescriptor, encoding),
      mode: stat.mode,
    };
  } finally {
    fs.closeSync(fileDescriptor);
  }
}

function readFileNoFollow(filePath, encoding) {
  return readFileWithMetadataNoFollow(filePath, encoding).content;
}

function readJsonNoFollow(filePath) {
  return JSON.parse(readFileNoFollow(filePath, 'utf8'));
}

View on GitHub (pinned to 8321021c54)