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
- Ensure every object key is constructed as `${companyId}/${relativePath}` before passing to get/put.
- Strip any leading slash from the relative path so the companyId prefix stays first.
- 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
- Always construct object keys as `${companyId}/${normalizedRelativePath}` at the call site.
- Never accept raw user input as an object key — sanitize and prefix first.
- Treat cross-company access as a security boundary; unit-test the prefix guard.
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
- Refusing to write export file outside output directory: ${re
- Invalid object key path.
- sync operation ${label} path is not a confined absolute path
- sync operation ${label} path escapes its confinement root: $
- provision stageFile.name must be a simple basename, got: ${s
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/7fa5da6a74e00c02.
Report an issue: GitHub.