santifer/career-ops · error · Error

cannot read

Error message

cannot read ${rel || '.'}: ${err.message}

What it means

hashPluginTree walks a plugin directory recursively (via its internal `walk` closure) and hashes every file to compute an integrity manifest. Before hashing, it calls readdirSync on each directory; if that fails (the directory cannot be opened/read), it throws this error wrapping the underlying message, with `rel || '.'` identifying the relative path that could not be read. It exists to convert a raw filesystem error into an error that names the plugin path being processed.

Solutions

  1. Verify the plugin directory exists and is readable: run `ls -la` on the reported path and fix permissions (chmod/chown) or correct the path passed to hashPluginTree.
  2. Reinstall or re-extract the plugin so the directory tree is complete and readable.
  3. Check the inner error message (the part after 'cannot read <path>: ') for the actual errno (ENOENT vs EACCES vs ELOOP) and fix accordingly.
  4. If a subdirectory within the plugin is intentionally unreadable, exclude it from the tree or fix its permissions.

Example fix

// before: hashing a path that may not exist
const { integrity } = hashPluginTree(userSuppliedDir);

// after: check readability first
const dir = userSuppliedDir;
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
  throw new Error(`plugin dir not found: ${dir}`);
}
const { integrity } = hashPluginTree(dir);
Defensive patterns

Strategy: try-catch

Validate before calling

import { existsSync, statSync, accessSync, constants } from 'fs';
function isReadableDir(p) {
  try {
    if (!existsSync(p) || !statSync(p).isDirectory()) return false;
    accessSync(p, constants.R_OK);
    return true;
  } catch { return false; }
}

Type guard

function isPluginDir(p) {
  return typeof p === 'string' && p.length > 0 && isReadableDir(p);
}

Try / catch

try {
  const { files, integrity } = hashPluginTree(dir);
} catch (err) {
  if (err.message.startsWith('cannot read ')) {
    const relPath = err.message.split('cannot read ')[1].split(':')[0].trim();
    console.error(`Plugin tree unreadable at '${relPath}' — check existence/permissions`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling hashPluginTree(dir) where `dir` itself does not exist, is not readable by the current user, or where any subdirectory within the tree (excluding node_modules/.git) cannot be read — e.g. a directory that is actually a broken symlink resolved earlier, a permissions-restricted directory, or a race where the directory is deleted during traversal.

Common situations: Plugin installed with root-only permissions but the tool runs as a normal user; a plugin directory removed or renamed after the path was captured (partial uninstall, IDE cleanup); passing a typo'd or non-existent plugins directory; running on a mount that went offline; on Windows, a directory locked by another process.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/e2a2bbb1cd467c37. Report an issue: GitHub.

Appendix: source

Thrown at plugins/_lock.mjs:42

function sha256(buf) {
  return 'sha256-' + createHash('sha256').update(buf).digest('hex');
}

/**
 * Hash EVERY regular file in a plugin directory tree, recursively. NOT a curated
 * subset — the entry can `import('./anything.mjs')`, so a partial hash would let
 * a rug-pull mutate an un-hashed file. Rejects symlinks (a symlinked file would
 * pass the hash while pointing elsewhere). Excludes node_modules + .git.
 *
 * @param {string} dir absolute plugin directory
 * @returns {{ files: Record<string,string>, integrity: string }}
 */
export function hashPluginTree(dir) {
  const files = {};
  const walk = (abs, rel) => {
    let entries;
    try { entries = readdirSync(abs, { withFileTypes: true }); }
    catch (err) { throw new Error(`cannot read ${rel || '.'}: ${err.message}`); }
    for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
      if (e.name === 'node_modules' || e.name === '.git') continue;
      const childAbs = path.join(abs, e.name);
      const childRel = rel ? `${rel}/${e.name}` : e.name;
      // lstat (not stat) so a symlink is detected, never followed.
      const st = lstatSync(childAbs);
      if (st.isSymbolicLink()) throw new Error(`refusing to hash symlink: ${childRel}`);
      if (st.isDirectory()) walk(childAbs, childRel);
      else if (st.isFile()) files[childRel] = sha256(readFileSync(childAbs));
      else throw new Error(`refusing to hash non-regular file: ${childRel}`);
    }
  };
  walk(dir, '');
  // Aggregate integrity = sha256 over the deterministic sorted "rel:hash" join.
  const aggregate = Object.keys(files).sort().map(k => `${k}:${files[k]}`).join('\n');
  return { files, integrity: sha256(Buffer.from(aggregate)) };
}

View on GitHub (pinned to aac998c7ed)