santifer/career-ops · error · Error

refusing to hash non-regular file: ${childRel}

Error message

refusing to hash non-regular file: ${childRel}

What it means

Thrown by `hashPluginTree` (plugins/_lock.mjs:52) when `lstatSync` reports a file that is neither a regular file nor a directory nor a symlink — i.e. a special file type such as a FIFO, socket, character/block device. The integrity walker can only meaningfully hash regular files, so an unknown file type aborts rather than silently skipping it (a skip would leave an un-hashed entry exploitable by a rug-pull).

Source

Thrown at plugins/_lock.mjs:52

 * @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)) };
}

/** Read plugins.lock (fail-open to an empty lock — like the rest of the engine). */
export function readLock(root) {
  const file = lockPath(root);
  if (!existsSync(file)) return { lockfileVersion: LOCK_VERSION, plugins: {} };
  try {
    const parsed = JSON.parse(readFileSync(file, 'utf8'));
    if (!parsed || typeof parsed !== 'object' || typeof parsed.plugins !== 'object') return { lockfileVersion: LOCK_VERSION, plugins: {} };
    return parsed;
  } catch {
    return { lockfileVersion: LOCK_VERSION, plugins: {} };

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Locate the special file: `find plugins/<name> -type p -o -type s -o -type b -o -type c`.
  2. Remove the offending file if it is not part of the plugin.
  3. If the plugin legitimately needs a runtime socket, ensure it is created at runtime in a writable dir (not bundled in the plugin tree that gets hashed).

Example fix

# before
mkfifo plugins/myplugin/job-queue   # accidentally left behind
# after
find plugins -type p -delete   # remove all FIFOs from the plugin tree
Defensive patterns

Strategy: validation

Validate before calling

import { lstatSync, readdirSync } from 'node:fs';
import path from 'node:path';
// Detect non-regular files before hashing.
function findSpecialFiles(root) {
  const bad = [];
  const walk = (dir) => {
    for (const e of readdirSync(dir, { withFileTypes: true })) {
      if (e.name === 'node_modules' || e.name === '.git') continue;
      const p = path.join(dir, e.name);
      const st = lstatSync(p);
      if (st.isDirectory()) walk(p);
      else if (!st.isFile()) bad.push(p);
    }
  };
  walk(root);
  return bad;
}
const special = findSpecialFiles(pluginDir);
if (special.length) throw new Error(`Remove special files: ${special.join(', ')}`);

Try / catch

try {
  hashPluginTree(pluginDir);
} catch (err) {
  if (/non-regular file/.test(err.message)) {
    console.error(`Clean the plugin dir: ${err.message}`);
  } else throw err;
}

Prevention

When it happens

Trigger: A plugin directory contains a named pipe, Unix domain socket, or device file. The walk hits it, `isFile()` and `isDirectory()` both return false, and the guard throws naming the relative path.

Common situations: A leftover FIFO/socket from a crashed dev tool or editor; a plugin dir accidentally placed in /tmp or /var where a daemon created a socket; someone ran `mkfifo` inside a plugin dir for testing; an accidental device-node copy on Linux.

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/e6ae1db4bc7c7e5e. Report an issue: GitHub.