{"record":{"id":"a395cfad29051bd8","repo":"abhigyanpatwari/GitNexus","slug":"filename-must-not-be-a-hard-link","errorCode":null,"errorMessage":"${filename} must not be a hard link","messagePattern":"(.+?) must not be a hard link","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/config/repo-control-file.ts","lineNumber":23,"sourceCode":"\n/** Read a bounded, regular control file owned by the repository root. */\nexport async function readRepoControlFile(\n  repoRoot: string,\n  filename: string,\n): Promise<string | null> {\n  const requestedRoot = path.resolve(repoRoot);\n  const requested = path.resolve(requestedRoot, filename);\n  const relative = path.relative(requestedRoot, requested);\n  if (relative.startsWith('..') || path.isAbsolute(relative)) {\n    throw new Error(`${filename} resolves outside the repository root`);\n  }\n\n  try {\n    const canonicalRoot = fs.realpathSync(requestedRoot);\n    const beforeOpen = fs.lstatSync(requested);\n    if (beforeOpen.isSymbolicLink()) throw new Error(`${filename} must not be a symbolic link`);\n    if (!beforeOpen.isFile()) throw new Error(`${filename} must be a regular file`);\n    if (beforeOpen.nlink !== 1) throw new Error(`${filename} must not be a hard link`);\n    if (beforeOpen.size > MAX_REPO_CONTROL_FILE_BYTES) {\n      throw new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`);\n    }\n    return await new Promise<string>((resolve, reject) => {\n      const stream = fs.createReadStream(requested, {\n        flags: 'r',\n        start: 0,\n        end: MAX_REPO_CONTROL_FILE_BYTES,\n        autoClose: true,\n      });\n      const chunks: Buffer[] = [];\n      let totalBytes = 0;\n      let validated = false;\n      let settled = false;\n\n      const finish = (value: string): void => {\n        if (settled) return;\n        settled = true;","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/config/repo-control-file.ts#L5-L41","documentation":"The pre-open lstat check in readRepoControlFile rejects files whose link count (nlink) is not exactly 1. Multiple hard links to the same inode mean the file content can be swapped from outside the repository without changing the path, which defeats the integrity assumptions of the control-file reader. The library therefore refuses to read any hard-linked control file.","triggerScenarios":"The control file at the requested path has been hard-linked elsewhere (nlink > 1) when readRepoControlFile is invoked via loadAnalyzeConfigStrict or content.","commonSituations":"Backup tools (rsync --link-dest, cp -al, Time Machine-style dedup) hard-link config files; a developer ran `ln` to share a config between repos; a container layer deduplicated files via hard links.","solutions":["Replace the hard-linked file with an independent copy: `cp --remove-destination <file> <file>` or `mv <file> tmp && cp tmp <file> && rm tmp`.","Find the other links with `fs.lstatSync(path).nlink` / `find <dir> -samefile <file>` and delete the extraneous ones.","Avoid hard-link-based backup/sync tools against the repository, or exclude control files from them.","Recreate the file from source control (`git checkout -- <file>`)."],"exampleFix":"// before: nlink=2 hard link shared with ~/backup\nln ~/.gitnexusrc ./.gitnexusrc\n// after: independent regular file\ncp --remove-destination ~/.gitnexusrc ./.gitnexusrc","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nconst st = fs.lstatSync(controlFilePath);\nif (st.nlink !== 1) {\n  console.error(`${controlFilePath} has ${st.nlink} hard links; replace with an independent copy`);\n}","typeGuard":"function hasSingleLink(p: string): boolean {\n  try { return fs.lstatSync(p).nlink === 1; } catch { return false; }\n}","tryCatchPattern":"try {\n  await readRepoControlFile(root, filename);\n} catch (err) {\n  if ((err as Error).message.includes('must not be a hard link')) {\n    const tmp = controlFilePath + '.copy';\n    fs.copyFileSync(controlFilePath, tmp);\n    fs.renameSync(tmp, controlFilePath); // new inode, nlink=1\n  } else throw err;\n}","preventionTips":["Avoid hard-link-based backup tools (cp -al, rsync --link-dest) on repositories.","Check nlink after restore/dedup operations.","Keep control files exclusive to one repo; copy instead of linking."],"tags":["filesystem","security","hardlink","integrity"],"backgroundTag":"hardlink-detected","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-09-01T13:15:02.810Z","contentChangedAt":"2026-09-01T13:15:02.810Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}