santifer/career-ops · error · Error
cannot read ${rel || '.'}: ${err.message}
Error message
cannot read ${rel || '.'}: ${err.message} What it means
Thrown inside `hashPluginTree` (plugins/_lock.mjs) when `readdirSync` fails on a directory while walking the plugin tree to compute its integrity hash. The path in the message is the relative path (`rel`) within the plugin dir, or '.' for the root. The underlying OS error (e.g. EACCES, ENOENT) is appended. This hash feeds plugins.lock, so a read failure aborts lock generation rather than silently producing a partial hash that a rug-pull could exploit.
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 9b17a8ac97)
Solutions
- Check the relative path in the message — confirm the plugin dir still exists: `ls -la plugins/<name>`.
- Fix permissions if EACCES: ensure the running user can read the plugin dir recursively (`chmod -R +r` or correct ownership).
- Re-run `node plugins.mjs install` if the dir was partially deleted, to restore a complete tree.
- If another process is mutating plugins concurrently, serialize plugin install/remove operations.
Defensive patterns
Strategy: validation
Validate before calling
import { accessSync, constants } from 'node:fs';
// Pre-check the plugin dir is readable before hashing.
function assertReadableDir(dir) {
try {
accessSync(dir, constants.R_OK);
} catch {
throw new Error(`Plugin dir not readable: ${dir} — fix permissions or reinstall.`);
}
}
assertReadableDir(pluginDir); Try / catch
try {
const { integrity } = hashPluginTree(pluginDir);
} catch (err) {
if (/cannot read/.test(err.message)) {
console.error(`Lock skipped — unreadable plugin dir: ${err.message}`);
// fail-open: skip lock for this plugin rather than aborting
} else throw err;
} Prevention
- Ensure consistent file ownership for plugin dirs.
- Avoid mutating plugin dirs while a scan/lock operation is running.
When it happens
Trigger: `hashPluginTree(dir)` is called on a plugin directory that has been deleted between discovery and hashing (ENOENT), has restrictive permissions (EACCES/EPERM), or sits on a filesystem that rejects readdir. Any code path that writes or verifies plugins.lock (lock generation, `node doctor.mjs --lock`, engine startup with a changed plugin) triggers the walk.
Common situations: A plugin dir was `rm -rf`'d mid-run (another process or a failed git operation); plugin files are owned by a different user and the current process lacks read permission; running on a read-only or network mount that intermittently fails readdir; a race where the lock writer runs while `node plugins.mjs remove` is deleting the dir.
Related errors
- refusing to hash non-regular file: ${childRel}
- refusing to hash symlink: ${childRel}
- portal-health lock timeout: ${lockDir} held > ${timeoutMs}ms
- Validation failed (${err.message}) and revert also failed ($
- LOCK_TIMEOUT
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/e2a2bbb1cd467c37.
Report an issue: GitHub.