paperclipai/paperclip · warning
additional source projectId is not a simple path segment: ${
Error message
additional source projectId is not a simple path segment: ${projectId} What it means
Thrown inside the additional-sources staging loop when projectId is empty or contains "/", "\\", or "..". The projectId is interpolated into a remote path (path.posix.join(runtimeRootDir, `project-${projectId}`)), so any path-like content would let a malicious or careless value escape the per-project directory or traverse the remote filesystem. This is a path-traversal guard.
Source
Thrown at packages/adapter-utils/src/remote-managed-runtime.ts:193
// Stage each referenced (additional) project as a plain, read-only tree in its
// OWN isolated remote directory (`project-<projectId>`). Additional sources
// never get the anchor's git-history/overlay semantics. Per-project failure
// isolation: one project's failure logs a warning and is skipped; the run and
// the other projects continue (no workspace restore, unlike an asset failure).
const additionalSourceDirs: Record<string, string> = {};
for (const source of input.additionalSources ?? []) {
const { localPath, projectId } = source;
try {
if (!path.posix.isAbsolute(localPath)) {
throw new Error(`additional source localPath is not an absolute path: ${localPath}`);
}
if (
projectId.length === 0 ||
projectId.includes("/") ||
projectId.includes("\\") ||
projectId.includes("..")
) {
throw new Error(`additional source projectId is not a simple path segment: ${projectId}`);
}
const remoteDir = path.posix.join(runtimeRootDir, `project-${projectId}`);
await syncDirectoryToSsh({
spec: input.spec,
localDir: localPath,
remoteDir,
exclude: REMOTE_ADDITIONAL_SOURCE_HEAVY_DIR_EXCLUDES,
onProgress: input.onProgress,
progressLabel: `project-${projectId}`,
});
additionalSourceDirs[projectId] = remoteDir;
} catch (error) {
console.warn(
`[paperclip] Failed to stage referenced project ${projectId}; skipping it. ${String(error)}`,
);
}
}
View on GitHub (pinned to 67001ec6eb)
Solutions
- Use a simple opaque identifier for projectId: alphanumeric, hyphen, underscore — no path separators or dot segments.
- If your IDs are org/repo slugs, sanitize them: slug.replace(/[\\/]+/g, "-").
- Reject empty projectIds at the config boundary; default to a stable hash or UUID if the caller does not provide one.
- Validate projectId against /^[A-Za-z0-9_-]+$/ before staging to fail fast with a clear upstream message.
Example fix
// before
additionalSources: [
{ localPath: "/srv/foo", projectId: "org/foo" },
{ localPath: "/srv/bar", projectId: "" },
]
// after
additionalSources: [
{ localPath: "/srv/foo", projectId: "org-foo" },
{ localPath: "/srv/bar", projectId: "bar" },
] Defensive patterns
Strategy: validation
Validate before calling
const PROJECT_ID_RE = /^[A-Za-z0-9_-]+$/;
function assertProjectId(id: string): void {
if (!PROJECT_ID_RE.test(id)) {
throw new Error(`projectId must be alphanumeric/hyphen/underscore (got ${id}); slashes and .. are rejected as path traversal`);
}
}
for (const source of input.additionalSources ?? []) assertProjectId(source.projectId); Type guard
function isSafeProjectId(value: unknown): value is string {
return typeof value === "string" && value.length > 0 && !value.includes("/") && !value.includes("\\") && !value.includes("..") && /^[A-Za-z0-9_-]+$/.test(value);
} Try / catch
// Per-source failures are already caught and logged as warnings by remote-managed-runtime.
// To make failures loud, pre-validate before staging:
for (const source of input.additionalSources ?? []) {
if (!isSafeProjectId(source.projectId)) {
throw new Error(`Unsafe projectId rejected: ${source.projectId}`);
}
} Prevention
- Use opaque alphanumeric project IDs; never put org/repo slugs in projectId.
- Sanitize slugs at the config boundary: slug.replace(/[\\/]+/g, "-").
- Reject empty projectIds upstream; default to a stable hash/UUID.
- Validate against /^[A-Za-z0-9_-]+$/ at schema load.
When it happens
Trigger: additionalSources: [{ localPath: "/abs/path", projectId: "" }], [{ ..., projectId: "../etc" }], [{ ..., projectId: "foo/bar" }], or [{ ..., projectId: "foo\\.." }]. Each is rejected at remote-managed-runtime.ts:187-194. Like 334, the throw is caught per-source and logged as a warning; the source is skipped, not fatal.
Common situations: User-supplied project identifiers that include slashes (org/repo slugs from GitHub), project IDs derived from filesystem paths, empty IDs from incomplete form submissions, or untrusted upstream payloads. The guard prevents these from being concatenated into the remote rsync target.
Related errors
- additional source localPath is not an absolute path: ${local
- Refusing to write export file outside output directory: ${re
- Invalid object key for company ${companyId}.
- Invalid object key path.
- Refusing to remove install store with an invalid manifest at
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/cff0c5c9581789f5.
Report an issue: GitHub.