{"record":{"id":"92457c0421c4f111","repo":"paperclipai/paperclip","slug":"provision-stagefile-name-must-be-a-simple-basename","errorCode":null,"errorMessage":"provision stageFile.name must be a simple basename, got: ${safeName}","messagePattern":"provision stageFile\\.name must be a simple basename, got: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/sandbox-managed-runtime.ts","lineNumber":963,"sourceCode":"      // ordered post-upload command. There is no native-diversion gate; a\n      // custom-provisioned asset's bytes now ride native `uploadFiles` and its\n      // command runs as a provider-executed post-upload command.\n      const assetTarPath = path.join(tempDir, `${asset.key}.tar`);\n      await createTarballFromDirectory({\n        localDir: asset.localDir,\n        archivePath: assetTarPath,\n        followSymlinks: asset.followSymlinks,\n        exclude: asset.exclude,\n      });\n      const files: SandboxSyncFileMapping[] = [\n        { sourcePath: assetTarPath, targetPath: remoteAssetTar, kind: \"file\", access: \"rw\", writablePath: remoteAssetDir },\n      ];\n      // Stage provision helper files (e.g. the merge scripts) into the temp dir\n      // and map them alongside the asset tar so they ride the same native upload.\n      for (const stageFile of asset.provision?.stageFiles ?? []) {\n        const safeName = stageFile.name;\n        if (/[\\\\/]|\\.\\.(\\.|$)/.test(safeName) || safeName === \"..\") {\n          throw new Error(`provision stageFile.name must be a simple basename, got: ${safeName}`);\n        }\n        const stageBytes = typeof stageFile.contents === \"string\"\n          ? Buffer.from(stageFile.contents)\n          : stageFile.contents;\n        const stageHostPath = path.join(tempDir, `${asset.key}.stage.${safeName}`);\n        await fs.writeFile(stageHostPath, stageBytes);\n        // A stage helper file (for example a merge script) is a read-only input\n        // that the provision command reads; the agent does not change it and does\n        // not keep it. So it is `access: \"ro\"` and never joins the writable set.\n        files.push({\n          sourcePath: stageHostPath,\n          targetPath: path.posix.join(runtimeRootDir, safeName),\n          kind: \"file\",\n          access: \"ro\",\n        });\n      }\n      const postUploadCommand = asset.provision?.postUploadCommand?.({\n        assetTarPath: remoteAssetTar,","sourceCodeStart":945,"sourceCodeEnd":981,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/sandbox-managed-runtime.ts#L945-L981","documentation":"Thrown while staging provision helper files for an asset when stageFile.name contains a path separator (forward or back slash), or matches `..`. Because the name is joined into both a host temp path and a remote target path (runtimeRootDir/name), accepting a non-basename would allow path injection. This is a basename validation guard on the provision.stageFiles array.","triggerScenarios":"An asset.provision.stageFiles entry whose .name is something like 'sub/merge.sh', '..\\evil', or '..'. Triggered during the asset provisioning loop in prepareSandboxManagedRuntime when custom provision scripts are configured.","commonSituations":"A skills/asset manifest author put a relative subpath in stageFile.name expecting it to create nested dirs; a Windows-style name with backslashes leaked in; copy-paste of a full script path into a basename-only field; malicious or malformed manifest.","solutions":["Use a plain basename (e.g. 'merge.sh') for stageFile.name and let the provision command reference it from the runtime root.","Validate names with the same regex /[\\\\/]|\\.\\.(\\.|$)/ in your manifest loader so bad data is rejected early.","If nested layout is required, encode it in the provision command (mkdir -p) rather than in the stage file name.","Sanitize user input by path.basename() before populating stageFile.name."],"exampleFix":"// before\nstageFiles: [{ name: 'scripts/merge.sh', contents: '...' }]\n// after\nstageFiles: [{ name: 'merge.sh', contents: '...' }]","handlingStrategy":"validation","validationCode":"const SAFE_NAME = /^[A-Za-z0-9._-]+$/;\nfunction assertBasename(name: string): void {\n  if (/[\\\\/]|\\.\\.(\\.|$)/.test(name) || name === '..' || !SAFE_NAME.test(name)) {\n    throw new Error(`stageFile.name must be a simple basename: ${name}`);\n  }\n}","typeGuard":"function isSimpleBasename(name: string): boolean {\n  return typeof name === 'string' && name.length > 0 && !/[\\\\/]|\\.\\.(\\.|$)/.test(name) && name !== '..';\n}","tryCatchPattern":null,"preventionTips":["Validate stageFile.name in manifest loaders before it reaches staging.","Use path.basename() to sanitize any user-derived name.","Encode nested layout in the provision command, not in the name."],"tags":["sandbox","security","path-traversal","asset","provision","validation"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}