affaan-m/ECC · error · Error

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

Error message

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

What it means

readFileWithMetadataNoFollow opens the path with O_RDONLY | O_NOFOLLOW (so the final component is not followed) and then fstat's the descriptor. If the descriptor does not point at a regular file (S_ISREG false — e.g. it is a directory, pipe, socket, device, or symlink target), reading is refused. This prevents the installer from reading attacker-controlled non-file entries as if they were file contents.

Source

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

    );
    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 01e15490f0)

Solutions

  1. Identify the path from the error message and run `ls -la <path>` to confirm its type.
  2. If it is a directory or symlink, remove or rename it: `rm -rf <path>` or `mv <path> <path>.bak`.
  3. Re-run the install/repair operation so a real file is written.
  4. Audit how the non-file entry was created (failed prior install, manual mkdir, sync tool) to prevent recurrence.

Example fix

// before: ~/.claude/install-state.json is a directory
// after:
rm -rf ~/.claude/install-state.json
./install.sh --target claude
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
function assertRegularFile(filePath) {
  const st = fs.lstatSync(filePath);
  if (st.isSymbolicLink()) throw new Error('path is a symlink');
  if (!st.isFile()) throw new Error('path is not a regular file');
}
// before reading via the library helpers
assertRegularFile(stateFilePath);

Type guard

function isReadableRegularFile(filePath) {
  try {
    return fs.lstatSync(filePath).isFile() && !fs.lstatSync(filePath).isSymbolicLink();
  } catch {
    return false;
  }
}

Try / catch

try {
  content = readJsonNoFollow(path);
} catch (err) {
  if (err.message.startsWith('Refusing to read non-file path')) {
    // remove or rename the offending entry, then regenerate state
  } else throw err;
}

Prevention

When it happens

Trigger: Any internal call to readFileNoFollow / readFileWithMetadataNoFollow / readJsonNoFollow / copyContainedFile when the resolved path is a directory, a symlink (blocked by O_NOFOLLOW at open), a FIFO, or a special file. Common during repair when an install-state points at a destination that has since become a directory.

Common situations: A destination recorded in the install-state file was replaced by a directory or a symlink; repair then tries to read it as a file. A previous failed install left a directory where a JSON file was expected (e.g. ~/.claude/install-state.json is a directory).

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/78a9de293ba2c0ad. Report an issue: GitHub.