JuliusBrussee/caveman · error

caveman build: imported source escapes project root

Error message

caveman build: imported source escapes project root

What it means

A relative import's resolved file lies outside the project root by both lexical path and realpath — the import genuinely reaches beyond the project (e.g. "../../.."). The builder's project closure must stay inside the canonical root; importing out of it is not lockable through the project path, so the build aborts with this error.

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 27d5a3981a)

Solutions

  1. Rewrite the import as a workspace/package dependency (bare specifier) declared in package.json so it resolves through the package closure path.
  2. Move the target file inside the project root and use a contained relative path.
  3. Fix stale ../ chains after restructuring — run tsc to list imports that no longer resolve inside the project.

Example fix

// before: project/a/b/src.ts
import { helper } from "../../../outside/helper.ts";

// after
import { helper } from "my-workspace-helper"; // declared dependency
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative, isAbsolute } from "node:path";
function importStaysInRoot(root: string, importer: string, spec: string): boolean {
  if (!spec.startsWith(".")) return true; // bare: package path
  const target = resolve(dirname(importer), spec);
  const rel = relative(root, target);
  return !isAbsolute(rel) && !rel.startsWith("..");
}

Try / catch

try {
  await buildSourceGraph(root);
} catch (error) {
  if (error instanceof Error && error.message === "caveman build: imported source escapes project root") {
    // convert the ../.. import into a declared package dependency or move the file inside
  } else throw error;
}

Prevention

When it happens

Trigger: A source file uses ../ or ../../ specifiers that climb past the project root directory, or a moved file kept its old deep-relative import which now escapes.

Common situations: Files moved into the project from a sibling directory without rewriting imports; projects nested inside a workspace where sources reach at workspace siblings instead of using package imports.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/50b6b0bf6bc96b1d. Report an issue: GitHub.