vercel-labs/agent-skills · error · Error

${label} must be a JSON object.

Error message

${label} must be a JSON object.

What it means

Thrown by assertObject() when the parsed JSON value is not a plain object — i.e. it is null, a primitive, or an array (Array.isArray check). mergeSignals requires both inputs to be objects before reading fields like schemaVersion/routes, so a top-level array or scalar is rejected. This catches cases where a file contains a JSON array of findings instead of the wrapper object.

Source

Thrown at skills/vercel-optimize/scripts/merge-signals.mjs:166

    else if (a === '--force') out.force = true;
    else out.positional.push(a);
  }
  out.signalsPath = out.positional[0];
  out.codebasePath = out.positional[1];
  return out;
}

async function readJson(path, label) {
  try {
    return JSON.parse(await readFile(path, 'utf-8'));
  } catch (err) {
    throw new Error(`Could not read ${label} JSON at ${path}: ${err.message}`);
  }
}

function assertObject(value, label) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(`${label} must be a JSON object.`);
  }
}

async function writeOutput(path, body, { force }) {
  if (!force && await exists(path)) {
    throw new Error(`output file already exists: ${path}. Use a fresh run directory or pass --force to overwrite.`);
  }
  await mkdir(dirname(path), { recursive: true });
  await writeFile(path, body);
}

async function exists(path) {
  try {
    await access(path);
    return true;
  } catch {
    return false;
  }

View on GitHub (pinned to b8caa260a4)

Solutions

  1. Re-run the producing step so it emits the full object wrapper, not a bare array.
  2. If you have a bare array, wrap it: `node -e 'const a=require("./x.json");require("fs").writeFileSync("x.json",JSON.stringify({findings:a},null,2))'` only if the schema expects it.
  3. Inspect the file's first non-whitespace character — `{` is required, not `[`.

Example fix

// before — codebase.json is a bare array
[ { "route": "/" } ]

// after — wrapped as scan-codebase output
{ "stack": {...}, "routes": [...], "findings": [...] }
Defensive patterns

Strategy: type-guard

Validate before calling

function assertPlainObject(value, label) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(`${label} must be a JSON object, got ${Array.isArray(value) ? 'array' : typeof value}`);
  }
}
assertPlainObject(parsed, 'signals');

Type guard

function isPlainJsonObject(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

Prevention

When it happens

Trigger: A signals.json whose top level is an array; a codebase.json that is `null` or a bare string/number; a file containing only `[...]` (e.g. raw findings list) instead of `{...}`.

Common situations: A scan/collect step wrote a bare array instead of the wrapper object; an empty file parsed as null; someone hand-edited the JSON down to a list.

Related errors


AI-assisted analysis of vercel-labs/agent-skills@b8caa260a4 (2026-08-13). Data as JSON: /api/errors/ec2d7d20854d3727. Report an issue: GitHub.