{"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":"error_code","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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/source-graph.ts#L78-L114","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Replace the out-of-root symlink with a real copy of the target file inside the project","If the target is shared source, bring it in as a workspace package / npm dependency instead of a symlink","Adjust the traversal stop-roots configuration if the external location is intentional","Remove or repoint stale symlinks created by an environment setup script"],"exampleFix":"// before: symlink escapes root\nln -s ~/shared/helpers.ts src/helpers.ts\n// after: vendored copy inside root\ncp ~/shared/helpers.ts src/helpers.ts","handlingStrategy":"validation","validationCode":"import { realpathSync } from 'fs';\nconst rp = realpathSync(fileInProject);\nif (!rp.startsWith(canonicalRoot + '/')) throw new Error(`symlink escapes root: ${fileInProject}`);","typeGuard":null,"tryCatchPattern":"try {\n  await build({ files: sourceFiles(entry) });\n} catch (e) {\n  if (e.message.includes('symlink escapes project root')) {\n    console.error('Replace out-of-root symlinks with vendored copies or workspace deps');\n  } else throw e;\n}","preventionTips":["Avoid symlinking source files to locations outside the repo","Use workspace packages for shared code instead of symlinks","Add a prebuild script that rewrites or flags escaping symlinks"],"tags":["build","security","symlink","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}