paperclipai/paperclip · error · Error

Export output path ${root} exists and is not a directory.

Error message

Export output path ${root} exists and is not a directory.

What it means

Thrown by confirmOverwriteExportDirectory when the resolved output path already exists as a file (or any non-directory entry) rather than a directory. The exporter needs to mkdir and write files under the root; a file blocking that path is a hard stop.

Source

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

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;

  // --force skips the guard for non-interactive/automated callers (e.g. the
  // nightly backup routine, which exports into a git clone that legitimately
  // still holds .git and BACKUP-README.md after cleaning tracked content).
  if (opts.force) return;

  if (!process.stdin.isTTY || !process.stdout.isTTY) {
    throw new Error(`Export output directory ${root} already contains files. Re-run interactively, pass --force, or choose an empty directory.`);
  }

  const confirmed = await p.confirm({
    message: `Overwrite existing files in ${root}?`,
    initialValue: false,
  });

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Choose a different --out path that is or will be a directory.
  2. Remove or rename the blocking file: 'rm <path>' (after confirming it is safe to delete).
  3. If the path is a symlink to a file, repoint or remove the symlink.
  4. Use a fresh directory name like 'export-<timestamp>'.

Example fix

# before
paperclipai company export --company cmp_abc --out backup.zip   # backup.zip is a file
# after
paperclipai company export --company cmp_abc --out backup-dir/
Defensive patterns

Strategy: validation

Validate before calling

async function ensureExportOutIsDir(outDir: string): Promise<void> {
  const fs = await import("fs/promises");
  const stat = await fs.stat(outDir).catch(() => null);
  if (stat && !stat.isDirectory()) {
    throw new Error(`Refusing export: ${outDir} is a file. Choose a directory path.`);
  }
}

Prevention

When it happens

Trigger: Passing --out pointing at an existing regular file (e.g. a previous export zip, a README, a socket). Pointing --out at a path where a symlink resolves to a file. Reusing a name that was a file output from another tool.

Common situations: User previously ran 'paperclipai company export --out backup.zip' (file) then tries '--out backup.zip' for a folder export. A symlink in the path resolves to a file. Conflicting output naming conventions between scripts.

Related errors


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