{"record":{"id":"8c475ebf7b863f96","repo":"JuliusBrussee/caveman","slug":"caveman-build-source-graph-symlink-escapes-projec","errorCode":null,"errorMessage":"caveman build: source graph symlink escapes project root","messagePattern":"caveman build: source graph symlink escapes project root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/source-graph.ts","lineNumber":96,"sourceCode":"          break;\n        } catch (error) {\n          if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n        }\n      }\n      if (!found) {\n        throw new Error(`caveman build: unresolved relative source import ${JSON.stringify(specifier)}`);\n      }\n      if (bare) {\n        await collectPackageClosure(packageRoot!, files, packageRoots);\n        continue;\n      }\n      const lexicalFound = found;\n      found = await realpath(found);\n      const insideRoot = isPathWithin(canonicalRoot, found);\n      if (!insideRoot && !bare && !external.has(path)) {\n        const lexicalInsideRoot = isPathWithin(canonicalRoot, lexicalFound);\n        if (lexicalInsideRoot) {\n          throw new Error(\"caveman build: source graph symlink escapes project root\");\n        }\n        throw new Error(\"caveman build: imported source escapes project root\");\n      }\n      if (!insideRoot) external.add(found);\n      if (!files.has(found)) {\n        files.add(found);\n        if (!traversalStopRoots.some((stop) => isPathWithin(stop, found))) {\n          queue.push(found);\n        }\n      }\n    }\n  }\n  return files;\n}\n\nfunction explicitImportCandidates(base: string, importer: string): string[] {\n  const importerExtension = extname(importer);\n  if (![\".ts\", \".tsx\", \".mts\", \".cts\"].includes(importerExtension)) return [base];","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/source-graph.ts#L78-L114","documentation":"A relative import resolved lexically to a file inside the project root, but its realpath (after following symlinks) lands outside the canonical root. The source graph locks files by physical identity; a symlink that escapes the project makes the closure non-finite/unverifiable from inside, so the build rejects it explicitly rather than silently following it out.","triggerScenarios":"A symlink inside the project directory points to a file outside the project (e.g. linking a shared module from ~/common or another checkout), and a project source imports through it.","commonSituations":"Developers symlinking shared utilities between repos; monorepo tooling that links packages into src; editor plugins or patch-package-style workflows creating symlinks inside the source tree.","solutions":["Remove the escaping symlink and either copy the file in or vendor it as a real dependency package installed through node_modules.","Move the shared code inside the project root so the physical file is contained.","If the target must stay outside, import it as a proper package dependency so it goes through the bare-specifier package closure path instead of a project-relative symlink."],"exampleFix":"# before\nln -s ~/shared/util.ts src/shared-util.ts # then import \"./shared-util\"\n\n# after\n# place the real file inside the project, or depend on it as a package:","handlingStrategy":"validation","validationCode":"import { realpath, lstat } from \"node:fs/promises\";\nimport { relative, isAbsolute, resolve, dirname } from \"node:path\";\nasync function noEscapingSymlinks(projectRoot: string): Promise<void> {\n  const root = await realpath(projectRoot);\n  for (const file of await listProjectFiles(projectRoot)) {\n    const st = await lstat(file);\n    if (st.isSymbolicLink()) {\n      const real = await realpath(file);\n      const rel = relative(root, real);\n      if (isAbsolute(rel) || rel.startsWith(\"..\")) throw new Error(`escaping symlink: ${file} -> ${real}`);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await buildSourceGraph(root);\n} catch (error) {\n  if (error instanceof Error && error.message === \"caveman build: source graph symlink escapes project root\") {\n    // find the symlink (compare lexical vs realpath) and remove it or vendor the target\n  } else throw error;\n}","preventionTips":["Never symlink shared source into the project; vendor it or depend on it as a package.","Run a pre-build scan for symlinks inside the project directory.","Remember the check is realpath-based: a link that looks inside can still resolve outside."],"tags":["build","symlink","filesystem","containment"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}