{"record":{"id":"99eac738bd3f5179","repo":"paperclipai/paperclip","slug":"sandbox-cwd-cwd-must-be-inside-workspacedir","errorCode":null,"errorMessage":"Sandbox cwd \"${cwd}\" must be inside workspaceDir \"${workspaceDir}\".","messagePattern":"Sandbox cwd \"(.+?)\" must be inside workspaceDir \"(.+?)\"\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/local-process-sandbox.ts","lineNumber":359,"sourceCode":"export async function buildLocalProcessSandboxSpawnTarget(input: {\n  executable: string;\n  args: string[];\n  cwd: string;\n  options: LocalProcessSandboxOptions;\n}): Promise<LocalProcessSandboxSpawnTarget> {\n  if (process.platform !== \"linux\") {\n    throw new Error(\"Local process filesystem and network scopes are currently supported only on Linux.\");\n  }\n  const filesystemScope = input.options.filesystemScope ?? null;\n  const networkScope = input.options.networkScope ?? null;\n  if (!filesystemScope && !networkScope) throw new Error(\"Local process sandbox requires a filesystem or network scope.\");\n\n  const workspaceDir = normalizeAbsolutePath(input.options.workspaceDir, \"Sandbox workspaceDir\");\n  const cwd = normalizeAbsolutePath(input.cwd, \"Sandbox cwd\");\n  if (filesystemScope === \"workspace\") {\n    const relativeCwd = path.relative(workspaceDir, cwd);\n    if (relativeCwd.startsWith(\"..\") || path.isAbsolute(relativeCwd)) {\n      throw new Error(`Sandbox cwd \"${cwd}\" must be inside workspaceDir \"${workspaceDir}\".`);\n    }\n    const outboundRestorePaths = (input.options.outboundRestorePaths ?? []).map((candidate, index) =>\n      normalizeAbsolutePath(candidate, `Sandbox outboundRestorePaths[${index}]`));\n    for (const [index, extraPath] of (input.options.extraPaths ?? []).entries()) {\n      if (extraPath.access !== \"rw\") continue;\n      const normalizedExtraPath = normalizeAbsolutePath(extraPath.path, `Sandbox extraPaths[${index}].path`);\n      const relativeToWorkspace = path.relative(workspaceDir, normalizedExtraPath);\n      const synchronized = !relativeToWorkspace.startsWith(\"..\") && !path.isAbsolute(relativeToWorkspace);\n      const restored = outboundRestorePaths.some((restorePath) => {\n        const relative = path.relative(restorePath, normalizedExtraPath);\n        return !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n      });\n      if (!synchronized && !restored) {\n        throw new Error(\n          `Writable sandbox path \"${normalizedExtraPath}\" is outside synchronized workspace \"${workspaceDir}\" and has no outbound restore mapping.`,\n        );\n      }\n    }","sourceCodeStart":341,"sourceCodeEnd":377,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/local-process-sandbox.ts#L341-L377","documentation":"Thrown inside buildLocalProcessSandboxSpawnTarget (filesystemScope === \"workspace\" branch) when path.relative(workspaceDir, cwd) yields a path that escapes the workspace — either starting with \"..\" or detected as absolute. The sandbox only bind-mounts workspaceDir read-write, so a cwd outside it would not exist inside the container; the check fails fast with a readable message instead of letting the spawned process fail with ENOENT.","triggerScenarios":"workspaceDir is /srv/work but cwd is /tmp/scratch, or workspaceDir is /home/user/repo but cwd is /home/user (a parent of the workspace). Symlinked paths that resolve outside the workspace also trigger this once normalizeAbsolutePath resolves them. The check at local-process-sandbox.ts:358-360 covers both \"..\" prefixes and absolute relatives.","commonSituations":"Adapter sets workspaceDir to the project root but inherits cwd from the parent process (e.g. process.cwd() at /), or vice versa. Also seen when workspaceDir is computed from a config key while cwd comes from a different source (env, CLI flag), and the two are not reconciled before sandbox spawn.","solutions":["Set cwd to a path inside workspaceDir before calling buildLocalProcessSandboxSpawnTarget — typically cwd === workspaceDir is the safe choice.","If the process genuinely needs to run from a parent directory, expand workspaceDir to that parent so cwd falls inside it.","Resolve symlinks on both paths (fs.realpath) before comparison so an in-workspace symlink that points outside is not silently rejected.","Reconcile the two paths at the config boundary: validate path.relative(workspaceDir, cwd) does not start with \"..\" before spawn."],"exampleFix":"// before\nbuildLocalProcessSandboxSpawnTarget({\n  executable: \"node\",\n  args: [\"script.js\"],\n  cwd: \"/tmp/scratch\",\n  options: { ...options, filesystemScope: \"workspace\", workspaceDir: \"/srv/work\" },\n});\n\n// after\nbuildLocalProcessSandboxSpawnTarget({\n  executable: \"node\",\n  args: [\"script.js\"],\n  cwd: \"/srv/work\",\n  options: { ...options, filesystemScope: \"workspace\", workspaceDir: \"/srv/work\" },\n});","handlingStrategy":"validation","validationCode":"function assertCwdInWorkspace(workspaceDir: string, cwd: string): void {\n  const rel = path.relative(workspaceDir, cwd);\n  if (rel.startsWith(\"..\") || path.isAbsolute(rel)) {\n    throw new Error(`cwd ${cwd} must be inside workspace ${workspaceDir}`);\n  }\n}\n\nawait fs.realpath(workspaceDir).then((real) => assertCwdInWorkspace(real, await fs.realpath(cwd)));","typeGuard":null,"tryCatchPattern":"try {\n  return await buildLocalProcessSandboxSpawnTarget(input);\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith(\"Sandbox cwd\") && error.message.includes(\"must be inside workspaceDir\")) {\n    const fixed = { ...input, cwd: input.options.workspaceDir };\n    return buildLocalProcessSandboxSpawnTarget(fixed);\n  }\n  throw error;\n}","preventionTips":["Default cwd to workspaceDir at the adapter boundary.","Resolve symlinks on both paths before comparison so an in-workspace link that points outside is not silently rejected.","Reconcile workspaceDir and cwd at config load: validate path.relative(workspaceDir, cwd) does not start with \"..\"."],"tags":["filesystem","sandbox","config-validation","paths"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}