paperclipai/paperclip · error

Invalid object key for company ${companyId}.

Error message

Invalid object key for company ${companyId}.

What it means

Thrown by assertStorageCompanyPrefix when a storage object key does not start with `${companyId}/` or contains a '..' substring. This is a tenant-isolation guard: object keys must be namespaced under the owning company so that one company cannot read or write another company's storage prefix. The '..' check blocks trivial path-traversal escapes.

Source

Thrown at cli/src/commands/worktree.ts:292

  return value.startsWith(WORKTREE_NAME_PREFIX) ? value : `${WORKTREE_NAME_PREFIX}${value}`;
}

function resolveWorktreeHome(explicit?: string): string {
  return explicit ?? process.env.PAPERCLIP_WORKTREES_DIR ?? DEFAULT_WORKTREE_HOME;
}

function resolveWorktreeStartPoint(explicit?: string): string | undefined {
  return explicit ?? nonEmpty(process.env.PAPERCLIP_WORKTREE_START_POINT) ?? undefined;
}

type ConfiguredStorage = {
  getObject(companyId: string, objectKey: string): Promise<Buffer>;
  putObject(companyId: string, objectKey: string, body: Buffer, contentType: string): Promise<void>;
};

function assertStorageCompanyPrefix(companyId: string, objectKey: string): void {
  if (!objectKey.startsWith(`${companyId}/`) || objectKey.includes("..")) {
    throw new Error(`Invalid object key for company ${companyId}.`);
  }
}

function normalizeStorageObjectKey(objectKey: string): string {
  const normalized = objectKey.replace(/\\/g, "/").trim();
  if (!normalized || normalized.startsWith("/")) {
    throw new Error("Invalid object key.");
  }
  const parts = normalized.split("/").filter((part) => part.length > 0);
  if (parts.length === 0 || parts.some((part) => part === "." || part === "..")) {
    throw new Error("Invalid object key.");
  }
  return parts.join("/");
}

function resolveLocalStoragePath(baseDir: string, objectKey: string): string {
  const resolved = path.resolve(baseDir, normalizeStorageObjectKey(objectKey));
  const root = path.resolve(baseDir);

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Ensure every object key is constructed as `${companyId}/${relativePath}` before passing to get/put.
  2. Strip any leading slash from the relative path so the companyId prefix stays first.
  3. Run normalizeStorageObjectKey on the relative portion first, then prefix with `${companyId}/`.

Example fix

// before
await storage.putObject(companyId, attachmentPath, body, ct);
// after
const safeKey = `${companyId}/${normalizeStorageObjectKey(attachmentPath)}`;
await storage.putObject(companyId, safeKey, body, ct);
Defensive patterns

Strategy: validation

Validate before calling

function safeCompanyKey(companyId: string, relativePath: string): string {
  const clean = relativePath.replace(/\\/g, '/').trim().replace(/^\/+/, '');
  if (!clean || clean.includes('..')) throw new Error('Invalid relative path');
  return `${companyId}/${clean}`;
}
// const key = safeCompanyKey(companyId, userPath);  // always prefixed, never traverses

Type guard

function isCompanyPrefixedKey(companyId: string, objectKey: string): boolean {
  return objectKey.startsWith(`${companyId}/`) && !objectKey.includes('..');
}

Prevention

When it happens

Trigger: Calling the worktree storage get/put flow (which calls assertStorageCompanyPrefix) with an objectKey that is unprefixed, prefixed with a different companyId, absolute, or contains '..'. Reached on every getObject/putObject invocation in the ConfiguredStorage adapter used by the worktree command.

Common situations: Caller building an object key from user input without prepending `${companyId}/`. Cross-company data migration that forgot the prefix. Bug where companyId is undefined so the prefix check string is 'undefined/'. Attempted traversal via '../../other-company/secret'.

Related errors


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