abhigyanpatwari/GitNexus · error

${filename} must be a regular file

Error message

${filename} must be a regular file

What it means

readRepoControlFile validates repository control files (e.g. .gitnexus config files) before reading them to prevent symlink/hardlink-based tampering. Before opening, it lstats the path and rejects anything that is not a regular file. This guard ensures the library never reads content from devices, directories, FIFOs, or other special file types.

Source

Thrown at gitnexus/src/config/repo-control-file.ts:22

export const MAX_REPO_CONTROL_FILE_BYTES = 1024 * 1024;

/** 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;

View on GitHub (pinned to 52924ef12c)

Solutions

  1. Check the path with `fs.lstatSync(path).isFile()` and remove/replace the non-regular entry.
  2. If a directory was created by mistake, remove it (`rm -r`) and create the expected regular file.
  3. Verify the configured filename/path passed to loadAnalyzeConfigStrict points at the intended regular file.
  4. Re-run the command; the file is re-validated on every read.

Example fix

// before: path is a directory/FIFO
rm -rf .gitnexusrc
// after: write a regular file
printf 'mode: strict\n' > .gitnexusrc
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
const st = fs.lstatSync(controlFilePath);
if (!st.isFile()) {
  throw new Error(`${controlFilePath} is not a regular file; remove it and create a plain file`);
}

Type guard

function isRegularFile(p: string): boolean {
  try { return fs.lstatSync(p).isFile(); } catch { return false; }
}

Try / catch

try {
  const content = await readRepoControlFile(root, filename);
} catch (err) {
  if ((err as Error).message.includes('must be a regular file')) {
    fs.rmSync(path.join(root, filename), { recursive: true, force: true });
    // recreate or skip
  } else throw err;
}

Prevention

When it happens

Trigger: Calling readRepoControlFile (directly or via loadAnalyzeConfigStrict / content) when the target path exists but lstat reports a non-regular type: a directory, FIFO, socket, device node, or symlink (symlink has its own message).

Common situations: A developer creates a directory where the control file is expected (e.g. mkdir .gitnexusrc by mistake); a misconfigured path points at /dev/stdin or a named pipe in CI; a provisioning script left a socket or fifo at the config path.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@52924ef12c (2026-09-01). Data as JSON: /api/errors/41e6529177a4fd71. Report an issue: GitHub.