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
- Sanitize each entry's relativePath before writing: strip leading slashes and any '..' segments.
- Regenerate the export from a trusted source so entries are properly root-relative.
- If you control the payload, ensure portable entries never contain '..' or absolute paths.
- 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
- Never trust relative paths from untrusted packages — sanitize first.
- Reject export entries containing '..' or leading slashes at ingestion time.
- Run exports into a dedicated empty directory to limit blast radius.
- Audit imported zip manifests before writing.
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
- Export output path ${root} exists and is not a directory.
- Export output directory ${root} already contains files. Re-r
- Invalid object key for company ${companyId}.
- Refusing to remove install store with an invalid manifest at
- Invalid install payload identifier '${identifier}'.
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/d99711e7e82d0f9d.
Report an issue: GitHub.