paperclipai/paperclip · error · Error

Unsafe ${label}: ${result}

Error message

Unsafe ${label}: ${result}

What it means

safeId validates that a string value (IDs, filenames, labels coming from config or JSON) matches the SAFE_ID pattern before it is used. Values containing path separators, spaces, or other unsafe characters are rejected with the label naming what field failed. It prevents untrusted roster/campaign content from injecting unsafe identifiers or paths.

Source

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

  "case.json",
  "config.json",
]);

function json(value) {
  return `${JSON.stringify(value, null, 2)}\n`;
}

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";
  }

View on GitHub (pinned to 01ad858492)

Solutions

  1. Rename the offending identifier to a slug matching SAFE_ID (typically letters, digits, hyphens/underscores) in the campaign or roster JSON.
  2. Trim whitespace from the ID in the source file.
  3. Check the SAFE_ID regex in the script and conform the value to it exactly.

Example fix

// before
"id": "Team Roster/v2"
// after
"id": "team-roster-v2"
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_ID = /^[a-z0-9][a-z0-9-_]*$/i;
if (!SAFE_ID.test(String(id ?? ""))) throw new Error(`ID must match ${SAFE_ID}: ${id}`);

Type guard

const isSafeId = (v) => typeof v === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(v);

Try / catch

try {
  id = safeId(rawId, "roster ID");
} catch (err) {
  if (err.message.startsWith("Unsafe")) console.error(`Rename identifier to slug form: ${err.message}`);
  throw err;
}

Prevention

When it happens

Trigger: Calling safeId(value, label) where String(value ?? '') does not match SAFE_ID — e.g. roster.id = 'my roster/v2', a campaign ID with spaces or dots, or an empty string.

Common situations: Hand-edited campaign/roster JSON with IDs containing slashes, spaces, or uppercase-unfriendly characters; a filename with '..' passed as a roster reference; copy-pasted IDs with trailing whitespace or special characters.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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