abhigyanpatwari/GitNexus · error

${filename} must not be a symbolic link

Error message

${filename} must not be a symbolic link

What it means

Thrown by readRepoControlFile in repo-control-file.ts after an lstat shows the requested control file is a symbolic link. Repo control files must be plain, first-party files inside the repo; symlinks could redirect reads to attacker-controlled or out-of-repo targets, so they are rejected outright as a TOCTOU-hardening measure.

Source

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

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 => {

View on GitHub (pinned to 52924ef12c)

Solutions

  1. Replace the symlink with a real file: `rm .gitnexusrc && cp /path/to/target .gitnexusrc`.
  2. If a dotfile manager created the link, configure it to copy instead of symlink for this path.
  3. Keep per-repo copies of the control file instead of sharing one via symlinks.
  4. Update test fixtures to copy files into the repo rather than linking them.

Example fix

// before
ln -s ~/dotfiles/.gitnexusrc .gitnexusrc

// after
rm .gitnexusrc
cp ~/dotfiles/.gitnexusrc .gitnexusrc
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
import path from 'node:path';
export function assertRegularFile(repoRoot: string, filename: string): void {
  const st = fs.lstatSync(path.resolve(repoRoot, filename));
  if (st.isSymbolicLink()) throw new Error(`${filename} must not be a symbolic link; replace with a real file`);
  if (!st.isFile()) throw new Error(`${filename} must be a regular file`);
}

Try / catch

try {
  const cfg = await readRepoControlFile(repoRoot, filename);
} catch (e) {
  if (e.message.includes('must not be a symbolic link')) {
    console.error(`Replace symlink ${filename} with a real file (rm + cp).`);
    process.exitCode = 1;
  } else throw e;
}

Prevention

When it happens

Trigger: Creating `ln -s ~/.gitnexusrc-global .gitnexusrc` in the repo, symlinked fixture files in a test checkout, package managers or dotfile managers (stow, chezmoi) that replace config files with symlinks, then running analyze with strict config loading.

Common situations: Dotfile management setups symlinking .gitnexusrc from a home repo; sharing one config across repos via symlink; build/test scripts linking fixture configs into the repo.

Related errors


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