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
- 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.
- Reinstall or re-extract the plugin so the directory tree is complete and readable.
- Check the inner error message (the part after 'cannot read <path>: ') for the actual errno (ENOENT vs EACCES vs ELOOP) and fix accordingly.
- 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
- Validate the plugin directory exists and is readable before hashing.
- Run the tool with a user that owns or can read the plugins directory.
- Avoid deleting/renaming plugin directories while hashing is in progress.
- Log the inner errno (ENOENT/EACCES) to distinguish missing vs permission issues.
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
- refusing to hash non-regular file
- refusing to hash symlink
- apify: JD cache write failed for
- Could not release report reservation
- ⚠️ Could not release report reservation
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)