affaan-m/ECC · error

Invalid : expected "hooks" to be a JSON object

Error message

Invalid ${label}: expected "hooks" to be a JSON object

What it means

When a Claude settings object contains a "hooks" key, its value must itself be a JSON object keyed by hook event names. This error is thrown when hooks exists but is a scalar, array, or null. The library only manages hook maps, so any other shape is rejected.

Solutions

  1. Change settings.hooks to an object mapping event names to arrays
  2. Remove the hooks key entirely if hooks are not used
  3. If hooks were disabled by setting null, use a different mechanism (e.g. remove managed entries) instead
  4. Validate the merged settings JSON with a schema before writing

Example fix

// before
{ "hooks": null }
// after
{ "hooks": { "PreToolUse": [] } }
Defensive patterns

Strategy: type-guard

Validate before calling

if ('hooks' in settings && (settings.hooks === null || typeof settings.hooks !== 'object' || Array.isArray(settings.hooks))) throw new Error('hooks must be an object');

Type guard

const isHooksMap = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);

Try / catch

try { return parseSettings(s); } catch (e) { if (e.message.includes('"hooks" to be a JSON object')) { const { hooks, ...rest } = s; return parseSettings(rest); } throw e; }

Prevention

When it happens

Trigger: Calling parseSettings/validatedSettings/validateSettings with a settings object where settings.hooks is not an object — e.g. "hooks": true, "hooks": [], or "hooks": null.

Common situations: Hand-merged settings where a hooks list overwrote the hooks map, a tool writing "hooks": null to 'disable' hooks, or schema drift from an older Claude settings format.

Related errors


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

Appendix: source

Thrown at scripts/lib/install/claude-settings.js:225

      if (seenIds.has(entry.id)) {
        throw new Error(
          `Invalid ${label}: expected unique id "${entry.id}" within event "${event}"`
        );
      }
      seenIds.add(entry.id);
    });
  }
  return cloneValue(managedHooks);
}

function validateSettings(settings, label = 'Claude settings') {
  if (!isJsonObject(settings)) {
    throw new Error(`Invalid ${label}: expected a JSON object`);
  }

  if (Object.prototype.hasOwnProperty.call(settings, 'hooks')) {
    if (!isJsonObject(settings.hooks)) {
      throw new Error(`Invalid ${label}: expected "hooks" to be a JSON object`);
    }
    for (const [event, entries] of Object.entries(settings.hooks)) {
      if (!Array.isArray(entries)) {
        throw new Error(`Invalid ${label}: expected hooks.${event} to be an array`);
      }
    }
  }

  return cloneValue(settings);
}

function replacePluginRootPlaceholders(value, pluginRoot) {
  if (typeof pluginRoot !== 'string') {
    throw new Error('Invalid Claude plugin root: expected a string');
  }
  if (typeof value === 'string') {
    return value.split(PLUGIN_ROOT_PLACEHOLDER).join(pluginRoot);
  }

View on GitHub (pinned to 8321021c54)