paperclipai/paperclip · error · Error

Unexpected attempt path ${join(source, entry.name)}

Error message

Unexpected attempt path ${join(source, entry.name)}

What it means

copyAttempt enumerates the source attempt directory and only accepts entries that are real files (not symlinks) whose names are in the ATTEMPT_FILES allowlist. Any subdirectory, symlink, or unexpected file causes an abort so that only known attempt artifacts are copied into the campaign.

Source

Thrown at packages/paperclip-runner/scripts/runner-protocol-eval-campaign.mjs:243

    Object.entries(entries)
      .map(([key, value]) => `${key}=${value}\n`)
      .join(""),
  );
}

async function copyAttempt(source, destination) {
  const metadata = await lstat(source);
  if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
    throw new Error(`Attempt source is not a real directory: ${source}`);
  }
  await mkdir(destination, { recursive: false, mode: 0o700 });
  for (const entry of await readdir(source, { withFileTypes: true })) {
    if (
      !entry.isFile() ||
      entry.isSymbolicLink() ||
      !ATTEMPT_FILES.has(entry.name)
    ) {
      throw new Error(`Unexpected attempt path ${join(source, entry.name)}`);
    }
    await cp(join(source, entry.name), join(destination, entry.name), {
      errorOnExist: true,
    });
  }
  for (const required of [
    "artifact.json",
    "score.json",
    "case.json",
    "config.json",
  ]) {
    await lstat(join(destination, required));
  }
}

async function findFiles(root, name) {
  const metadata = await lstat(root).catch(() => null);
  if (!metadata) return [];

View on GitHub (pinned to 01ad858492)

Solutions

  1. Remove the unexpected file/directory from the attempt folder (keep only files named in ATTEMPT_FILES).
  2. If the new file is a legitimate attempt artifact, add its exact name to the ATTEMPT_FILES set in the script.
  3. Clean OS-generated files (.DS_Store, Thumbs.db) before copying.

Example fix

// before
const ATTEMPT_FILES = new Set(["result.json", "log.txt"]);
// after
const ATTEMPT_FILES = new Set(["result.json", "log.txt", "trace.json"]);
Defensive patterns

Strategy: validation

Validate before calling

const entries = await readdir(source, { withFileTypes: true });
const bad = entries.filter((e) => !e.isFile() || e.isSymbolicLink() || !ATTEMPT_FILES.has(e.name));
if (bad.length > 0) {
  console.error("Unexpected entries:", bad.map((e) => e.name).join(", "));
}

Type guard

function isAllowedAttemptEntry(e) {
  return e.isFile() && !e.isSymbolicLink() && ATTEMPT_FILES.has(e.name);
}

Try / catch

try {
  await copyAttempt(source, dest);
} catch (err) {
  if (err.message.startsWith("Unexpected attempt path")) {
    console.error(err.message, "— remove the file or extend ATTEMPT_FILES");
    process.exit(1);
  }
  throw err;
}

Prevention

When it happens

Trigger: The attempt directory contains a file not listed in ATTEMPT_FILES, a subdirectory, or a symlinked entry; leftover logs, editor swap files (e.g. .DS_Store, vim swp), or partial-download files inside the attempt folder.

Common situations: A crashed run wrote extra output files into the attempt directory; OS artifact files (.DS_Store) created by browsing; someone added a new artifact file without updating ATTEMPT_FILES; an agent wrote debug output into the attempt folder.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/9b90d451dbc8ca2d. Report an issue: GitHub.