paperclipai/paperclip · error · Error

escapes its declared root

Error message

${label} escapes its declared root

What it means

inside() resolves a candidate path against a declared root and rejects any path that escapes that root: empty relative result (candidate equals root), '..', or any path starting with '../'. It is a path-traversal guard applied to campaign roster and config paths so untrusted JSON cannot reference files outside the eval program directory.

Solutions

  1. Move the referenced file inside the declared root directory and update the JSON to a relative path within it.
  2. Remove any '..' segments or absolute paths from roster/config fields in the campaign file.
  3. If a file must be shared, symlink or copy it into the program root instead of referencing it across the boundary.

Example fix

// before
"roster": "../../shared/roster.json"
// after: copy roster.json into evals/paperclip-runner/rosters/ and use
"roster": "rosters/roster.json"
Defensive patterns

Strategy: validation

Validate before calling

const rel = relative(resolve(root), resolve(candidate));
if (!rel || rel === ".." || rel.startsWith(`..${sep}`)) throw new Error(`${label} escapes its declared root`);

Type guard

const staysInside = (root, candidate) => {
  const rel = relative(resolve(root), resolve(candidate));
  return Boolean(rel) && rel !== ".." && !rel.startsWith(`..${require("path").sep}`);
};

Try / catch

try {
  rosterPath = inside(programRoot, lane.roster, "Campaign roster");
} catch (err) {
  if (err.message.endsWith("escapes its declared root")) console.error(`Use a path relative to and inside the program root for: ${lane.roster}`);
  throw err;
}

Prevention

When it happens

Trigger: Calling inside(root, candidate, label) where candidate is absolute outside root, uses '..' segments, or equals the root itself — e.g. lane.roster = '../secrets/roster.json' or an absolute path to another directory.

Common situations: A roster or config field in campaign JSON referencing a shared file via '../'; moving the campaign file so relative paths now resolve outside the root; typos like '../../rosters/x.json'; malicious or accidentally absolute paths.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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

Appendix: source

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

async function loadObject(path) {
  const value = JSON.parse(await readFile(path, "utf8"));
  if (value === null || Array.isArray(value) || typeof value !== "object") {
    throw new Error(`Expected a JSON object: ${path}`);
  }
  return value;
}

function safeId(value, label = "identifier") {
  const result = String(value ?? "");
  if (!SAFE_ID.test(result)) throw new Error(`Unsafe ${label}: ${result}`);
  return result;
}

function inside(root, candidate, label) {
  const rel = relative(resolve(root), resolve(candidate));
  if (!rel || rel === ".." || rel.startsWith(`..${sep}`)) {
    throw new Error(`${label} escapes its declared root`);
  }
  return resolve(candidate);
}

export function credentialForConfig(config) {
  if (config.provider === "opencode") return "OPENROUTER_API_KEY";
  if (config.provider === "claude_managed") return "ANTHROPIC_API_KEY";
  if (config.provider === "aws_agentcore") return "AWS_AGENTCORE_OIDC";
  if (config.provider === "codex" || config.provider === undefined) {
    return "OPENAI_API_KEY";
  }
  if (config.provider === "acpx") {
    if (config.acpxAgent === "pi") return "OPENROUTER_API_KEY";
    if (config.acpxAgent === "claude") return "ANTHROPIC_API_KEY";
    if (config.acpxAgent === "codex") return "OPENAI_API_KEY";
  }
  throw new Error(
    `No credential policy for ${config.provider ?? "codex"}/${config.acpxAgent ?? "default"}`,

View on GitHub (pinned to 01ad858492)