{"record":{"id":"500de35277c56949","repo":"affaan-m/ECC","slug":"refusing-to-read-a-non-file-at-filepath","errorCode":null,"errorMessage":"Refusing to read a non-file at ${filePath}","messagePattern":"Refusing to read a non-file at (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install/opencode-legacy-migration.js","lineNumber":107,"sourceCode":"      return { status: 'invalid', state: null, error: null };\n    }\n    return { status: 'valid', state, error: null };\n  } catch (error) {\n    return {\n      status: 'unreadable',\n      state: null,\n      error: `Unable to inspect legacy OpenCode install-state at ${location.installStatePath}: ${error.message}`,\n    };\n  }\n}\n\nfunction hashFileNoFollow(filePath) {\n  const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);\n  const descriptor = fs.openSync(filePath, flags);\n  try {\n    const before = fs.fstatSync(descriptor, { bigint: true });\n    if (!before.isFile()) {\n      throw new Error(`Refusing to read a non-file at ${filePath}`);\n    }\n    const content = fs.readFileSync(descriptor);\n    const after = fs.fstatSync(descriptor, { bigint: true });\n    const finalPathStat = fs.lstatSync(filePath, { bigint: true });\n    const unchanged = before.dev === after.dev\n      && before.ino === after.ino\n      && before.size === after.size\n      && before.mtimeMs === after.mtimeMs\n      && before.ctimeMs === after.ctimeMs\n      && after.dev === finalPathStat.dev\n      && after.ino === finalPathStat.ino\n      && after.size === finalPathStat.size\n      && after.mtimeMs === finalPathStat.mtimeMs\n      && after.ctimeMs === finalPathStat.ctimeMs;\n    if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile() || !unchanged) {\n      throw new Error(`Refusing to read a file that changed during validation: ${filePath}`);\n    }\n    return {","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install/opencode-legacy-migration.js#L89-L125","documentation":"hashFileNoFollow() opens a file with O_NOFOLLOW (refusing to follow symlinks) and verifies via fstat that the opened descriptor is a regular file before reading it. If the path is not a regular file (directory, FIFO, device, or a filesystem without O_NOFOLLOW resolution), it throws this error instead of hashing it. This is a hardening measure so legacy-migration verification never reads through symlinks or special files.","triggerScenarios":"Calling hashFileNoFollow() (via verifyManagedLegacyFile) on a path that is a symlink to a file, a directory, a FIFO/socket, or a device node; an attacker or misconfiguration replacing an expected managed file with a symlink; a platform where O_NOFOLLOW is unavailable so the opened inode turns out to be non-regular.","commonSituations":"Dotfile managers (stow, chezmoi, GNU stow-style farms) that symlink ~/.claude contents instead of copying them, so managed legacy files are symlinks; a directory accidentally placed where a file is expected; security tooling flagging symlink swaps during migration verification.","solutions":["Replace the symlink (or non-file) at the path with a real regular file copy: `cp --remove-destination <target> <path>`.","If your dotfile manager symlinks this file, configure it to copy real files for ECC-managed paths.","Verify what is at the path with `ls -la` / `file <path>` to confirm whether it is a symlink, directory, or other non-file.","If migration verification cannot proceed legitimately, remove the legacy file and re-run migration from a genuine source file."],"exampleFix":"// before: path is a symlink\nln -s ~/dotfiles/CLAUDE.md ~/.claude/CLAUDE.md\n// after: real file\ncp --remove-destination ~/dotfiles/CLAUDE.md ~/.claude/CLAUDE.md","handlingStrategy":"validation","validationCode":"const fs = require('fs');\nconst st = fs.lstatSync(filePath);\nif (!st.isFile()) {\n  throw new Error(`${filePath} is not a regular file (symlink/dir?) — replace with a real file`);\n}","typeGuard":"function isRegularFileNoFollow(p) {\n  try { return fs.lstatSync(p).isFile(); } catch { return false; }\n}","tryCatchPattern":"try {\n  const hash = hashFileNoFollow(filePath);\n} catch (err) {\n  if (err.message.startsWith('Refusing to read a non-file')) {\n    console.error(`${filePath} is a symlink or special file; materialize a real file copy first`);\n    process.exitCode = 1;\n  } else throw err;\n}","preventionTips":["Configure dotfile managers to copy (not symlink) ECC-managed files.","Run ls -la over managed paths before migration to spot symlink substitutions.","Never replace managed files with symlinks or FIFOs; keep them regular files.","Re-run migration from genuine source files rather than pointing it at links."],"tags":["security","symlink","file"],"backgroundTag":"incompatible-source-type","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}