affaan-m/ECC · error

is invalid at : expected a JSON object

Error message

${label} is invalid at ${filePath}: expected a JSON object

What it means

readJsonObject() additionally enforces that the parsed value is a plain JSON object. If the file parses to null, a primitive, or an array, it throws this error stating a JSON object was expected. It guards inventory/state readers against structurally wrong files that are nonetheless valid JSON.

Solutions

  1. Wrap the file's contents in a JSON object — e.g. change `["agents"]` to `{ "selectedModules": ["agents"] }` matching the expected schema.
  2. Verify you are pointing at the correct file (the label in the message tells you which document was expected).
  3. Restore the original object-shaped file from git or regenerate it by rerunning the install flow.
  4. If generating this file in tooling, ensure you serialize an object, never an array or scalar, at the top level.

Example fix

// before (state.json)
["agents", "skills"]
// after
{ "selectedModules": ["agents", "skills"], "operations": [] }
Defensive patterns

Strategy: type-guard

Validate before calling

const value = JSON.parse(fs.readFileSync(statePath, 'utf8'));
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
  throw new Error(`${statePath} must contain a top-level JSON object`);
}

Type guard

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

Try / catch

try {
  const state = readJsonObject(statePath, 'Managed Claude install-state');
} catch (err) {
  if (err.message.includes('expected a JSON object')) {
    console.error(`${statePath} has the wrong top-level type; wrap contents in an object`);
    process.exitCode = 1;
  } else throw err;
}

Prevention

When it happens

Trigger: Calling readJsonObject(filePath, label) on a file whose top level is an array (e.g. a bare JSON list of modules), a string/number, or the literal `null`; pointing the reader at a different file that happens to be valid JSON but not an object.

Common situations: Someone 'simplifying' state files to an array; exporting a list from another tool into the state path; an empty-file edge where content like `[]` or `"..."` replaced the object; wrong file passed as the state/inventory path.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/c9d15036aedc9175. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/install/inventory.js:39

    || path.join(homeDir, '.claude');
  const projectRoot = options.projectRoot || process.cwd();

  return {
    homeDir: path.resolve(homeDir),
    configDir: path.resolve(configDir),
    projectRoot: path.resolve(projectRoot),
  };
}

function readJsonObject(filePath, label) {
  let value;
  try {
    value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
  } catch (error) {
    throw new Error(`${label} is invalid at ${filePath}: ${error.message}`);
  }
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error(`${label} is invalid at ${filePath}: expected a JSON object`);
  }
  return value;
}

function findManualClaudePlugin(options = {}) {
  const { configDir } = resolveClaudePaths(options);
  const pluginsDir = path.join(configDir, 'plugins');
  const candidates = [
    ['ecc', '.claude-plugin', 'plugin.json'],
    ['ecc', 'plugin.json'],
    ['ecc@ecc', '.claude-plugin', 'plugin.json'],
    ['ecc@ecc', 'plugin.json'],
    ['everything-claude-code', '.claude-plugin', 'plugin.json'],
    ['everything-claude-code', 'plugin.json'],
  ];

  for (const segments of candidates) {
    const manifestPath = path.join(pluginsDir, ...segments);

View on GitHub (pinned to 8321021c54)