JuliusBrussee/caveman · error

caveman build: source graph symlink escapes project root

Error message

caveman build: source graph symlink escapes project root

What it means

After lexically resolving an import inside the project, the walker takes its realpath and requires it to stay within the canonical project root. When the lexical path is inside the root but the realpath escapes it, a symlink is leading outside the project, which the build refuses with this dedicated message.

Solutions

  1. Replace the out-of-root symlink with a real copy of the target file inside the project
  2. If the target is shared source, bring it in as a workspace package / npm dependency instead of a symlink
  3. Adjust the traversal stop-roots configuration if the external location is intentional
  4. Remove or repoint stale symlinks created by an environment setup script

Example fix

// before: symlink escapes root
ln -s ~/shared/helpers.ts src/helpers.ts
// after: vendored copy inside root
cp ~/shared/helpers.ts src/helpers.ts
Defensive patterns

Strategy: validation

Validate before calling

import { realpathSync } from 'fs';
const rp = realpathSync(fileInProject);
if (!rp.startsWith(canonicalRoot + '/')) throw new Error(`symlink escapes root: ${fileInProject}`);

Try / catch

try {
  await build({ files: sourceFiles(entry) });
} catch (e) {
  if (e.message.includes('symlink escapes project root')) {
    console.error('Replace out-of-root symlinks with vendored copies or workspace deps');
  } else throw e;
}

Prevention

When it happens

Trigger: expandSourceGraph resolves an in-root relative import to a path that realpath() shows points outside canonicalRoot (symlink to a file elsewhere), while the symlink itself sits inside the project.

Common situations: Symlinking node_modules or src files to a shared location outside the repo; workspace links pointing at absolute paths outside the project root; copying the project with dangling/re-pointed symlinks.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/8c475ebf7b863f96. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/source-graph.ts:96

          break;
        } catch (error) {
          if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
        }
      }
      if (!found) {
        throw new Error(`caveman build: unresolved relative source import ${JSON.stringify(specifier)}`);
      }
      if (bare) {
        await collectPackageClosure(packageRoot!, files, packageRoots);
        continue;
      }
      const lexicalFound = found;
      found = await realpath(found);
      const insideRoot = isPathWithin(canonicalRoot, found);
      if (!insideRoot && !bare && !external.has(path)) {
        const lexicalInsideRoot = isPathWithin(canonicalRoot, lexicalFound);
        if (lexicalInsideRoot) {
          throw new Error("caveman build: source graph symlink escapes project root");
        }
        throw new Error("caveman build: imported source escapes project root");
      }
      if (!insideRoot) external.add(found);
      if (!files.has(found)) {
        files.add(found);
        if (!traversalStopRoots.some((stop) => isPathWithin(stop, found))) {
          queue.push(found);
        }
      }
    }
  }
  return files;
}

function explicitImportCandidates(base: string, importer: string): string[] {
  const importerExtension = extname(importer);
  if (![".ts", ".tsx", ".mts", ".cts"].includes(importerExtension)) return [base];

View on GitHub (pinned to 3ee70a1026)