abhigyanpatwari/GitNexus · error

must not traverse symbolic links or junctions

Error message

${TRUSTED_CACHE_DIRECTORY_ENV} must not traverse symbolic links or junctions

What it means

The final trust check for GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR compares the lexically resolved path (path.resolve of the normalized spelling) against the kernel-resolved realpath. If they differ, the configured spelling traverses a symlink or junction somewhere along the path (an ancestor component, even if the final component is real). Because the variable is an explicit trust assertion, its exact spelling must bind to the real directory the cache will use — otherwise a later symlink swap could redirect the cache undetected.

Solutions

  1. Point the variable at the fully real path: run realpath <dir> and export the printed value.
  2. On macOS, prefer $HOME-based paths or /private/tmp instead of /tmp.
  3. Replace the symlinked ancestor usage with the underlying mount path in CI scripts.
  4. Alternatively unset the variable and let GitNexus use its default secure cache location, which resolves this itself.

Example fix

# before
export GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR=/tmp/gnx-id-cache   # /tmp -> /private/tmp
# ...must not traverse symbolic links or junctions

# after
mkdir -p "$HOME/.cache/gnx-id-cache"
export GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR="$(realpath "$HOME/.cache/gnx-id-cache")"
Defensive patterns

Strategy: validation

Validate before calling

// Reject spellings that traverse symlinks before launching:
import { resolve, isAbsolute } from 'node:path';
import { realpathSync } from 'node:fs';
function spellingIsRealPath(dir: string): boolean {
  if (!isAbsolute(dir)) return false;
  try {
    return resolve(dir) === realpathSync(dir);
  } catch {
    return false;
  }
}

Try / catch

try {
  spawnSync('gitnexus', ['analyze']);
} catch (err) {
  if (String((err as Error).message).includes('must not traverse symbolic links or junctions')) {
    process.env.GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR = realpathSync(process.env.GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR!);
    return spawnSync('gitnexus', ['analyze']);
  }
  throw err;
}

Prevention

When it happens

Trigger: GITNEXUS_ANALYZER_IDENTITY_CACHE_DIR is absolute and names an existing real directory, but some ancestor is a symlink: e.g. /tmp/gnx on macOS (/tmp -> /private/tmp), /mnt/data where /mnt/data is a bind/symlink, a Windows junction, or a Docker volume symlink. path.resolve(normalized) != realpathSync.native(normalized) throws.

Common situations: macOS /tmp and /var aliases, Linux symlinked home directories (/home/user -> /mnt/data/user), Windows junctions in CI agent layouts, and any deployment where convenience symlinks sit in the path prefix.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@52924ef12c (2026-08-20). Data as JSON: /api/errors/770c4166f1231143. Report an issue: GitHub.

Appendix: source

Thrown at gitnexus/src/core/analyzer-identity.ts:2087

  }
  const normalized = path.normalize(configured);
  let resolved: string;
  try {
    const link = lstatSync(normalized);
    if (!link.isDirectory() || link.isSymbolicLink()) {
      throw new Error('not a real directory');
    }
    resolved = realpathSync.native(normalized);
  } catch {
    throw new Error(
      `${TRUSTED_CACHE_DIRECTORY_ENV} must name a pre-existing protected non-symlink directory`,
    );
  }
  // Reject junctions/symlinked ancestors as well as a symlink final component.
  // The environment variable is an explicit trust assertion, but its spelling
  // must still bind exactly to the directory the cache will use.
  if (!pathsEqual(path.resolve(normalized), resolved)) {
    throw new Error(`${TRUSTED_CACHE_DIRECTORY_ENV} must not traverse symbolic links or junctions`);
  }
  return resolved;
}

function cacheDirectory(
  options: AnalyzerIdentityResolveOptions,
  packageRoot: string,
  buildRoot: string,
): string | null {
  // An explicit location is a trusted operator/test override and therefore
  // remains authoritative, including when the secure default is unavailable.
  if (options.cacheDirectory) {
    const explicit = path.resolve(options.cacheDirectory);
    try {
      // Create it before any build/dependency directory guards are captured.
      // A cache nested immediately under a package root then changes that
      // parent's directory state once, not after we persist the first entry.
      mkdirSync(explicit, { recursive: true, mode: 0o700 });

View on GitHub (pinned to 52924ef12c)