paperclipai/paperclip · error · Error

Refusing to write export file outside output directory: ${re

Error message

Refusing to write export file outside output directory: ${relativePath}

What it means

Thrown by resolveExportOutputPath when a relative export entry, after path.resolve against the output root, escapes the root directory. This is a path-traversal guard: zip entries or portable file entries whose relativePath uses '../' or an absolute path would otherwise write anywhere on disk.

Source

Thrown at cli/src/commands/client/company.ts:1230

  for (const [relativePath, content] of Object.entries(exported.files)) {
    const normalized = relativePath.replace(/\\/g, "/");
    const filePath = resolveExportOutputPath(root, normalized);
    await mkdir(path.dirname(filePath), { recursive: true });
    const writeValue = portableFileEntryToWriteValue(content);
    if (typeof writeValue === "string") {
      await writeFile(filePath, writeValue, "utf8");
    } else {
      await writeFile(filePath, writeValue);
    }
  }
}

export function resolveExportOutputPath(root: string, relativePath: string): string {
  const resolvedRoot = path.resolve(root);
  const filePath = path.resolve(resolvedRoot, relativePath);
  const rootPrefix = resolvedRoot.endsWith(path.sep) ? resolvedRoot : `${resolvedRoot}${path.sep}`;
  if (filePath !== resolvedRoot && !filePath.startsWith(rootPrefix)) {
    throw new Error(`Refusing to write export file outside output directory: ${relativePath}`);
  }
  return filePath;
}

export async function confirmOverwriteExportDirectory(
  outDir: string,
  opts: { force?: boolean } = {},
): Promise<void> {
  const root = path.resolve(outDir);
  const stats = await stat(root).catch(() => null);
  if (!stats) return;
  if (!stats.isDirectory()) {
    throw new Error(`Export output path ${root} exists and is not a directory.`);
  }

  const entries = await readdir(root);
  if (entries.length === 0) return;

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Sanitize each entry's relativePath before writing: strip leading slashes and any '..' segments.
  2. Regenerate the export from a trusted source so entries are properly root-relative.
  3. If you control the payload, ensure portable entries never contain '..' or absolute paths.
  4. Audit the export file with 'unzip -l' (or equivalent) and reject packages containing traversal paths.

Example fix

// before (malicious/buggy entry)
resolveExportOutputPath("/out", "../../etc/crontab");
// after (guard at the source of the payload)
const safe = relativePath.replace(/\\/g, "/").replace(/^(\.{1,2}\/)+/, "");
resolveExportOutputPath("/out", safe);
Defensive patterns

Strategy: validation

Validate before calling

function sanitizeRelativePath(p: string): string {
  let s = p.replace(/\\/g, "/").replace(/^\/+/, "");
  const segments = s.split("/").filter((seg) => seg && seg !== "." && !(seg === ".."));
  return segments.join("/");
}
// apply to every entry before resolveExportOutputPath

Type guard

function isSafeRelativePath(root: string, p: string): boolean {
  const path = require("path");
  const resolvedRoot = path.resolve(root);
  const filePath = path.resolve(resolvedRoot, p);
  const prefix = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
  return filePath === resolvedRoot || filePath.startsWith(prefix);
}

Prevention

When it happens

Trigger: A malformed/tampered export payload containing entries like '../../etc/passwd', '/etc/foo', or absolute Windows paths. A portable export produced by a buggy exporter that recorded absolute paths instead of relative ones. Symlink edge cases where path.resolve leaves the target outside the root.

Common situations: Importing a company package sourced from an untrusted third party. A bug in the export format that stored root-relative paths with a leading slash. Cross-platform path separators (backslash) confusing the prefix check on a non-Windows host.

Related errors


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