paperclipai/paperclip · error · HttpError

outside_workspace

outside_workspace

Error message

Workspace file path is outside the workspace

What it means

normalizeWorkspaceRelativePath rejected a candidate workspace path before any filesystem access: it is empty, over MAX_RELATIVE_PATH_BYTES, contains NUL, or uses a file:// URL (each reported as code invalid_path). This is pure input validation on the caller-supplied relative path string, not a filesystem condition.

Source

Thrown at server/src/services/workspace-file-resources.ts:303

}

function normalizeWorkspaceRelativePath(input: string): NormalizedPath {
  const trimmed = input.trim();
  if (!trimmed) throw unprocessable("Workspace file path is required", { code: "invalid_path" });
  if (Buffer.byteLength(trimmed, "utf8") > MAX_RELATIVE_PATH_BYTES) {
    throw unprocessable("Workspace file path is too long", { code: "invalid_path" });
  }
  if (trimmed.includes("\0")) throw unprocessable("Workspace file path contains an invalid character", { code: "invalid_path" });
  if (/^file:\/\//i.test(trimmed)) throw unprocessable("File URLs are not supported", { code: "invalid_path" });
  if (/^[a-zA-Z]:/.test(trimmed)) throw unprocessable("Windows drive paths are not supported", { code: "invalid_path" });
  if (trimmed.startsWith("~")) throw unprocessable("Home-relative paths are not supported", { code: "invalid_path" });
  if (trimmed.includes("\\")) throw unprocessable("Workspace file paths must use forward slashes", { code: "invalid_path" });
  if (path.posix.isAbsolute(trimmed)) throw unprocessable("Workspace file path must be relative", { code: "invalid_path" });

  const normalizedRaw = path.posix.normalize(trimmed);
  const normalized = normalizedRaw.endsWith("/") ? normalizedRaw.replace(/\/+$/, "") : normalizedRaw;
  if (normalized === "." || normalized === ".." || normalized.startsWith("../")) {
    throw new HttpError(403, "Workspace file path is outside the workspace", { code: "outside_workspace" });
  }

  return {
    relativePath: normalized,
    segments: normalized.split("/").filter(Boolean),
  };
}

function denyReasonForPathSegments(segments: string[]): string | null {
  const lowerSegments = segments.map((segment) => segment.toLowerCase());
  if (lowerSegments.some((segment) => DENIED_SEGMENTS.has(segment))) return "denied_path_segment";

  const fileName = lowerSegments.at(-1) ?? "";
  if (fileName === ".env" || fileName.startsWith(".env.")) return "denied_secret";
  if (fileName.endsWith(".pem") || fileName.endsWith(".key") || fileName.endsWith(".p12") || fileName.endsWith(".pfx")) {
    return "denied_secret";
  }
  if (["id_rsa", "id_ed25519", ".npmrc", ".pypirc", ".netrc", "kubeconfig"].includes(fileName)) return "denied_secret";

View on GitHub (pinned to 120ae5428f)

Solutions

  1. Use a path inside the workspace root; path traversal outside the workspace is rejected.
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at server/src/services/workspace-file-resources.ts:303 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/e5f15ccd6c14b4ca. Report an issue: GitHub.