{"record":{"id":"3ca28f2688881cd7","repo":"paperclipai/paperclip","slug":"post-upload-command-cwd-escapes-the-operation-s-ta","errorCode":null,"errorMessage":"post-upload command cwd escapes the operation's target root: ${raw}","messagePattern":"post-upload command cwd escapes the operation's target root: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/command-managed-runtime.ts","lineNumber":194,"sourceCode":" * default to the runtime's stable command cwd at exec time.\n */\nexport function assertPostUploadCommandsConfined(operations: readonly SandboxSyncOperation[]): void {\n  for (const operation of operations) {\n    const commands = operation.postUploadCommands ?? [];\n    if (commands.length === 0) continue;\n    const targetRoots = operation.files.map((mapping) => path.posix.normalize(mapping.targetPath));\n    for (const command of commands) {\n      if (command.cwd == null) continue;\n      const raw = command.cwd;\n      if (!path.posix.isAbsolute(raw) || raw.split(\"/\").includes(\"..\")) {\n        throw new Error(`post-upload command cwd is not a confined absolute POSIX path: ${raw}`);\n      }\n      const normalized = path.posix.normalize(raw);\n      const within = targetRoots.some(\n        (root) => normalized === root || normalized.startsWith(`${root}/`),\n      );\n      if (!within) {\n        throw new Error(`post-upload command cwd escapes the operation's target root: ${raw}`);\n      }\n    }\n  }\n}\n\nexport function createCommandManagedRuntimeClient(input: {\n  runner: CommandManagedRuntimeRunner;\n  commandCwd: string;\n  timeoutMs: number;\n  shellCommand?: \"bash\" | \"sh\" | null;\n}): SandboxManagedRuntimeClient {\n  const shellCommand = preferredShellForSandbox(input.shellCommand);\n  const runShell = async (\n    script: string,\n    opts: {\n      stdin?: string;\n      timeoutMs?: number;\n      onLog?: (stream: \"stdout\" | \"stderr\", chunk: string) => Promise<void>;","sourceCodeStart":176,"sourceCodeEnd":212,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/command-managed-runtime.ts#L176-L212","documentation":"Thrown by assertPostUploadCommandsConfined (Security Condition C2) when a post-upload command's cwd is an absolute POSIX path with no '..' but does not equal and is not a subdirectory of any of the operation's file-mapping targetPaths. After normalizing both the cwd and all targetPaths, the check requires the cwd to start with '<root>/' or exactly equal a root. This prevents commands from running in arbitrary sandbox directories outside the uploaded file set.","triggerScenarios":"Calling client.syncIn(operations) where a postUploadCommand has a valid absolute cwd like '/etc' or '/tmp' that is not under any of the operation's file-mapping targetPath values. For example, files map to '/workspace/app' but the command cwd is '/workspace/other'.","commonSituations":"Configuring post-upload commands that operate in a different directory than the uploaded files. Typing a targetPath that doesn't exactly match the cwd prefix. Expecting the cwd to be a parent of the targetPath (the check only allows the cwd to be the root or under it, not above it).","solutions":["Ensure the cwd is exactly one of the targetPaths or a subdirectory of one (e.g., if targetPath is '/workspace/app', cwd can be '/workspace/app' or '/workspace/app/subdir').","If the command needs to run in a parent directory, add a file mapping for that directory so it becomes a valid target root.","Remove the cwd property to use the runtime's default command cwd instead.","Review all file mappings in the operation and align the cwd to one of their targetPaths."],"exampleFix":"// before: cwd is a sibling of the targetPath\nconst ops: SandboxSyncOperation[] = [{\n  files: [{ kind: \"directory\", sourcePath: \"./app\", targetPath: \"/workspace/app\" }],\n  postUploadCommands: [{ command: \"npm run build\", cwd: \"/workspace/scripts\" }],\n}];\n\n// after: cwd confined under targetPath\nconst ops: SandboxSyncOperation[] = [{\n  files: [{ kind: \"directory\", sourcePath: \"./app\", targetPath: \"/workspace/app\" }],\n  postUploadCommands: [{ command: \"npm run build\", cwd: \"/workspace/app\" }],\n}];","handlingStrategy":"validation","validationCode":"import path from 'node:path';\n\nfunction isCwdWithinTargetRoots(cwd: string, targetRoots: string[]): boolean {\n  const normalized = path.posix.normalize(cwd);\n  return targetRoots.some((root) => normalized === root || normalized.startsWith(`${root}/`));\n}\n\n// Call before client.syncIn:\nfor (const op of operations) {\n  const targetRoots = op.files.map((m) => path.posix.normalize(m.targetPath));\n  for (const cmd of op.postUploadCommands ?? []) {\n    if (cmd.cwd && !isCwdWithinTargetRoots(cmd.cwd, targetRoots)) {\n      throw new Error(`cwd ${cmd.cwd} must be within one of: ${targetRoots.join(', ')}`);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.syncIn(operations);\n} catch (error) {\n  if (error instanceof Error && error.message.includes('escapes the operation')) {\n    // Align the cwd to be within a targetPath\n    console.error('Post-upload cwd must be within a file-mapping targetPath:', error.message);\n  }\n  throw error;\n}","preventionTips":["Map every directory a post-upload command needs to operate in as a file mapping targetPath in the same operation.","The cwd cannot be a parent of the targetPath—only the root itself or a subdirectory beneath it.","If a command needs to run in a shared/parent directory, either omit cwd (use default '/') or add a file mapping for that directory.","Test operations with assertPostUploadCommandsConfined before deploying to catch confinement violations early."],"tags":["security","sandbox","path-confinement","adapter-utils"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}