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

  1. Use a simple opaque identifier for projectId: alphanumeric, hyphen, underscore — no path separators or dot segments.
  2. If your IDs are org/repo slugs, sanitize them: slug.replace(/[\\/]+/g, "-").
  3. Reject empty projectIds at the config boundary; default to a stable hash or UUID if the caller does not provide one.
  4. 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

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


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