abhigyanpatwari/GitNexus · error
${filename} must not be a hard link
Error message
${filename} must not be a hard link What it means
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.
Source
Thrown at gitnexus/src/config/repo-control-file.ts:23
/** Read a bounded, regular control file owned by the repository root. */
export async function readRepoControlFile(
repoRoot: string,
filename: string,
): Promise<string | null> {
const requestedRoot = path.resolve(repoRoot);
const requested = path.resolve(requestedRoot, filename);
const relative = path.relative(requestedRoot, requested);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error(`${filename} resolves outside the repository root`);
}
try {
const canonicalRoot = fs.realpathSync(requestedRoot);
const beforeOpen = fs.lstatSync(requested);
if (beforeOpen.isSymbolicLink()) throw new Error(`${filename} must not be a symbolic link`);
if (!beforeOpen.isFile()) throw new Error(`${filename} must be a regular file`);
if (beforeOpen.nlink !== 1) throw new Error(`${filename} must not be a hard link`);
if (beforeOpen.size > MAX_REPO_CONTROL_FILE_BYTES) {
throw new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`);
}
return await new Promise<string>((resolve, reject) => {
const stream = fs.createReadStream(requested, {
flags: 'r',
start: 0,
end: MAX_REPO_CONTROL_FILE_BYTES,
autoClose: true,
});
const chunks: Buffer[] = [];
let totalBytes = 0;
let validated = false;
let settled = false;
const finish = (value: string): void => {
if (settled) return;
settled = true;View on GitHub (pinned to 52924ef12c)
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>`).
Example fix
// before: nlink=2 hard link shared with ~/backup ln ~/.gitnexusrc ./.gitnexusrc // after: independent regular file cp --remove-destination ~/.gitnexusrc ./.gitnexusrc
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
const st = fs.lstatSync(controlFilePath);
if (st.nlink !== 1) {
console.error(`${controlFilePath} has ${st.nlink} hard links; replace with an independent copy`);
} Type guard
function hasSingleLink(p: string): boolean {
try { return fs.lstatSync(p).nlink === 1; } catch { return false; }
} Try / catch
try {
await readRepoControlFile(root, filename);
} catch (err) {
if ((err as Error).message.includes('must not be a hard link')) {
const tmp = controlFilePath + '.copy';
fs.copyFileSync(controlFilePath, tmp);
fs.renameSync(tmp, controlFilePath); // new inode, nlink=1
} else throw err;
} Prevention
- 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.
When it happens
Trigger: The control file at the requested path has been hard-linked elsewhere (nlink > 1) when readRepoControlFile is invoked via loadAnalyzeConfigStrict or content.
Common situations: 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.
Related errors
- Path traversal denied
- ${filename} must be a regular file
- ${filename} moved or was replaced while being opened
- Refusing symlinked auto-sync quarantine root: ${quarantineRo
- Cloning from private/internal addresses is not allowed
AI-assisted analysis of abhigyanpatwari/GitNexus@52924ef12c (2026-09-01).
Data as JSON: /api/errors/a395cfad29051bd8.
Report an issue: GitHub.