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
- 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.
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
- 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.
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
- sync operation ${label} path is not a confined absolute path
- sync operation ${label} path escapes its confinement root: $
- Daytona sync ${label} path is not a confined absolute path:
- Daytona sync ${label} path escapes the workspace remote dir:
- Refusing to write export file outside output directory: ${re
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/92457c0421c4f111.
Report an issue: GitHub.