{"record":{"id":"e6ae1db4bc7c7e5e","repo":"santifer/career-ops","slug":"refusing-to-hash-non-regular-file-childrel","errorCode":null,"errorMessage":"refusing to hash non-regular file: ${childRel}","messagePattern":"refusing to hash non-regular file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/_lock.mjs","lineNumber":52,"sourceCode":" * @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\n/** Read plugins.lock (fail-open to an empty lock — like the rest of the engine). */\nexport function readLock(root) {\n  const file = lockPath(root);\n  if (!existsSync(file)) return { lockfileVersion: LOCK_VERSION, plugins: {} };\n  try {\n    const parsed = JSON.parse(readFileSync(file, 'utf8'));\n    if (!parsed || typeof parsed !== 'object' || typeof parsed.plugins !== 'object') return { lockfileVersion: LOCK_VERSION, plugins: {} };\n    return parsed;\n  } catch {\n    return { lockfileVersion: LOCK_VERSION, plugins: {} };","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/_lock.mjs#L34-L70","documentation":"hashPluginTree only knows how to hash directories (recursed) and regular files (sha256 of contents). Any other filesystem entry type — FIFOs, sockets, device files, and other specials — causes this throw. It prevents the hasher from blocking forever on a FIFO read or producing nondeterministic hashes from special files, keeping the integrity manifest well-defined.","triggerScenarios":"Calling hashPluginTree(dir) when the tree contains a named pipe, Unix socket, device node, or similar non-regular/non-directory entry created accidentally (e.g. a process wrote its socket into the plugin dir) or maliciously (a crafted plugin archive).","commonSituations":"A dev server left a .sock file inside the plugin folder; a test created a FIFO in the plugin directory; extracting a malicious or malformed plugin tarball containing device nodes; a docker bind-mount surfacing odd node types.","solutions":["Identify the entry from the message (`find <plugins-dir> ! -type f ! -type d`) and delete it if it is transient (socket/FIFO left by a process).","Stop the process that creates the special file inside the plugin tree, or move its working directory elsewhere.","Re-install the plugin from a clean source if the archive itself contains special files.","Re-run hashing after the tree contains only regular files and directories."],"exampleFix":"// before: stale socket in plugin dir aborts hashing\n$ ls plugins/myplugin/\nserver.sock  index.mjs\n\n// after: remove non-regular entries, keep only real files\n$ rm plugins/myplugin/server.sock\n$ find plugins/myplugin ! -type f ! -type d -delete","handlingStrategy":"validation","validationCode":"import { readdirSync, lstatSync } from 'fs';\nimport path from 'path';\nfunction findSpecialFiles(dir, rel = '') {\n  const out = [];\n  for (const e of readdirSync(dir, { withFileTypes: true })) {\n    if (e.name === 'node_modules' || e.name === '.git') continue;\n    const childRel = rel ? `${rel}/${e.name}` : e.name;\n    const st = lstatSync(path.join(dir, e.name));\n    if (!st.isDirectory() && !st.isFile()) out.push(childRel);\n    else if (st.isDirectory()) out.push(...findSpecialFiles(path.join(dir, e.name), childRel));\n  }\n  return out;\n}\n// before calling: findSpecialFiles(pluginDir).length === 0","typeGuard":null,"tryCatchPattern":"try {\n  hashPluginTree(pluginDir);\n} catch (err) {\n  if (err.message.startsWith('refusing to hash non-regular file: ')) {\n    console.error(`Remove the FIFO/socket/device entry: ${err.message}`);\n  }\n  throw err;\n}","preventionTips":["Keep processes (dev servers, test runners) from creating sockets/FIFOs inside the plugins directory.","Clean transient artifacts (sockets, pipes) from plugin dirs before hashing.","Only install plugins from archives that contain regular files and directories."],"tags":["filesystem","plugin-integrity","hashing","security"],"backgroundTag":"unsupported-operation","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}