{"record":{"id":"e2a2bbb1cd467c37","repo":"santifer/career-ops","slug":"cannot-read-rel-err-message","errorCode":null,"errorMessage":"cannot read ${rel || '.'}: ${err.message}","messagePattern":"cannot read (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_lock.mjs","lineNumber":42,"sourceCode":"function sha256(buf) {\n  return 'sha256-' + createHash('sha256').update(buf).digest('hex');\n}\n\n/**\n * Hash EVERY regular file in a plugin directory tree, recursively. NOT a curated\n * subset — the entry can `import('./anything.mjs')`, so a partial hash would let\n * a rug-pull mutate an un-hashed file. Rejects symlinks (a symlinked file would\n * pass the hash while pointing elsewhere). Excludes node_modules + .git.\n *\n * @param {string} dir absolute plugin directory\n * @returns {{ files: Record<string,string>, integrity: string }}\n */\nexport function hashPluginTree(dir) {\n  const files = {};\n  const walk = (abs, rel) => {\n    let entries;\n    try { entries = readdirSync(abs, { withFileTypes: true }); }\n    catch (err) { throw new Error(`cannot read ${rel || '.'}: ${err.message}`); }\n    for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {\n      if (e.name === 'node_modules' || e.name === '.git') continue;\n      const childAbs = path.join(abs, e.name);\n      const childRel = rel ? `${rel}/${e.name}` : e.name;\n      // lstat (not stat) so a symlink is detected, never followed.\n      const st = lstatSync(childAbs);\n      if (st.isSymbolicLink()) throw new Error(`refusing to hash symlink: ${childRel}`);\n      if (st.isDirectory()) walk(childAbs, childRel);\n      else if (st.isFile()) files[childRel] = sha256(readFileSync(childAbs));\n      else throw new Error(`refusing to hash non-regular file: ${childRel}`);\n    }\n  };\n  walk(dir, '');\n  // Aggregate integrity = sha256 over the deterministic sorted \"rel:hash\" join.\n  const aggregate = Object.keys(files).sort().map(k => `${k}:${files[k]}`).join('\\n');\n  return { files, integrity: sha256(Buffer.from(aggregate)) };\n}\n","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/_lock.mjs#L24-L60","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { accessSync, constants } from 'node:fs';\n// Pre-check the plugin dir is readable before hashing.\nfunction assertReadableDir(dir) {\n  try {\n    accessSync(dir, constants.R_OK);\n  } catch {\n    throw new Error(`Plugin dir not readable: ${dir} — fix permissions or reinstall.`);\n  }\n}\nassertReadableDir(pluginDir);","typeGuard":null,"tryCatchPattern":"try {\n  const { integrity } = hashPluginTree(pluginDir);\n} catch (err) {\n  if (/cannot read/.test(err.message)) {\n    console.error(`Lock skipped — unreadable plugin dir: ${err.message}`);\n    // fail-open: skip lock for this plugin rather than aborting\n  } else throw err;\n}","preventionTips":["Ensure consistent file ownership for plugin dirs.","Avoid mutating plugin dirs while a scan/lock operation is running."],"tags":["plugin","lock","filesystem","permissions","integrity"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}