JuliusBrussee/caveman · error

caveman build: imported source escapes project root

Error message

caveman build: imported source escapes project root

What it means

When a resolved import's realpath is outside the canonical project root and the lexical path was also outside (i.e. not a symlink escape), the walker refuses to include the file in the build graph with this error. It guards against the source graph silently pulling in arbitrary files from elsewhere on disk.

Solutions

  1. Move the imported file into the project or convert it to a proper package dependency (bare import)
  2. Remove/fix the out-of-root absolute or ../-heavy import path
  3. If the external location is legitimate, add it to the allowed external set so it's treated as a boundary instead of graph content
  4. Check for editor auto-import misconfiguration that inserts absolute out-of-repo paths

Example fix

// before
import { config } from '/Users/dev/shared/config.ts';
// after
import { config } from './config';
Defensive patterns

Strategy: validation

Validate before calling

import { resolve } from 'path';
const target = resolve(dirname(importerPath), specifier);
if (!target.startsWith(canonicalRoot + '/')) throw new Error(`import escapes root: ${specifier}`);

Try / catch

try {
  await build({ files: sourceFiles(entry) });
} catch (e) {
  if (e.message.includes('imported source escapes project root')) {
    console.error('Convert the import to an in-repo file or npm dependency');
  } else throw e;
}

Prevention

When it happens

Trigger: expandSourceGraph follows a non-bare import whose resolved location (already outside root) is not registered as external, and the lexical path also lies outside canonicalRoot.

Common situations: Importing an absolute path outside the repo (e.g. '../../..../other-project/file.ts'); IDE auto-imports grabbing files from open workspace folders; generated imports pointing at out-of-repo artifacts.

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/50b6b0bf6bc96b1d. Report an issue: GitHub.

Appendix: source

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

          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];
  const extension = extname(base);
  const stem = base.slice(0, -extension.length);

View on GitHub (pinned to 3ee70a1026)