paperclipai/paperclip · error

provision stageFile.name must be a simple basename, got: ${s

Error message

provision stageFile.name must be a simple basename, got: ${safeName}

What it means

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.

Source

Thrown at packages/adapter-utils/src/sandbox-managed-runtime.ts:963

      // ordered post-upload command. There is no native-diversion gate; a
      // custom-provisioned asset's bytes now ride native `uploadFiles` and its
      // command runs as a provider-executed post-upload command.
      const assetTarPath = path.join(tempDir, `${asset.key}.tar`);
      await createTarballFromDirectory({
        localDir: asset.localDir,
        archivePath: assetTarPath,
        followSymlinks: asset.followSymlinks,
        exclude: asset.exclude,
      });
      const files: SandboxSyncFileMapping[] = [
        { sourcePath: assetTarPath, targetPath: remoteAssetTar, kind: "file", access: "rw", writablePath: remoteAssetDir },
      ];
      // Stage provision helper files (e.g. the merge scripts) into the temp dir
      // and map them alongside the asset tar so they ride the same native upload.
      for (const stageFile of asset.provision?.stageFiles ?? []) {
        const safeName = stageFile.name;
        if (/[\\/]|\.\.(\.|$)/.test(safeName) || safeName === "..") {
          throw new Error(`provision stageFile.name must be a simple basename, got: ${safeName}`);
        }
        const stageBytes = typeof stageFile.contents === "string"
          ? Buffer.from(stageFile.contents)
          : stageFile.contents;
        const stageHostPath = path.join(tempDir, `${asset.key}.stage.${safeName}`);
        await fs.writeFile(stageHostPath, stageBytes);
        // A stage helper file (for example a merge script) is a read-only input
        // that the provision command reads; the agent does not change it and does
        // not keep it. So it is `access: "ro"` and never joins the writable set.
        files.push({
          sourcePath: stageHostPath,
          targetPath: path.posix.join(runtimeRootDir, safeName),
          kind: "file",
          access: "ro",
        });
      }
      const postUploadCommand = asset.provision?.postUploadCommand?.({
        assetTarPath: remoteAssetTar,

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Use a plain basename (e.g. 'merge.sh') for stageFile.name and let the provision command reference it from the runtime root.
  2. Validate names with the same regex /[\\/]|\.\.(\.|$)/ in your manifest loader so bad data is rejected early.
  3. If nested layout is required, encode it in the provision command (mkdir -p) rather than in the stage file name.
  4. Sanitize user input by path.basename() before populating stageFile.name.

Example fix

// before
stageFiles: [{ name: 'scripts/merge.sh', contents: '...' }]
// after
stageFiles: [{ name: 'merge.sh', contents: '...' }]
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_NAME = /^[A-Za-z0-9._-]+$/;
function assertBasename(name: string): void {
  if (/[\\/]|\.\.(\.|$)/.test(name) || name === '..' || !SAFE_NAME.test(name)) {
    throw new Error(`stageFile.name must be a simple basename: ${name}`);
  }
}

Type guard

function isSimpleBasename(name: string): boolean {
  return typeof name === 'string' && name.length > 0 && !/[\\/]|\.\.(\.|$)/.test(name) && name !== '..';
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/92457c0421c4f111. Report an issue: GitHub.