affaan-m/ECC · error · Error

Invalid hooks config at ${hooksSourcePath}: expected "hooks"

Error message

Invalid hooks config at ${hooksSourcePath}: expected "hooks" to be a JSON object

What it means

Thrown by buildResolvedClaudeHooks in scripts/lib/install/apply.js. This function runs only when plan.adapter.target is 'claude' or 'claude-project'. It reads the source hooks.json, substitutes ${CLAUDE_PLUGIN_ROOT} placeholders via replacePluginRootPlaceholders, then requires the resulting hooks field to be a non-null, non-array object (matching Claude Code's hooks schema). If not, the installer refuses to write the hooks file.

Source

Thrown at scripts/lib/install/apply.js:247

  if (!plan.adapter || (plan.adapter.target !== 'claude' && plan.adapter.target !== 'claude-project')) {
    return null;
  }

  const pluginRoot = plan.targetRoot;
  const hooksDestinationPath = path.join(plan.targetRoot, 'hooks', 'hooks.json');
  const hooksOperation = findHooksOperation(plan, hooksDestinationPath);
  if (!hooksOperation) {
    return null;
  }
  const hooksSourcePath = hooksOperation.sourcePath;
  if (!fs.existsSync(hooksSourcePath)) {
    return null;
  }

  const hooksConfig = readJsonObject(hooksSourcePath, 'hooks config');
  const resolvedHooks = replacePluginRootPlaceholders(hooksConfig.hooks, pluginRoot);
  if (!resolvedHooks || typeof resolvedHooks !== 'object' || Array.isArray(resolvedHooks)) {
    throw new Error(`Invalid hooks config at ${hooksSourcePath}: expected "hooks" to be a JSON object`);
  }

  return {
    hooksOperation,
    hooksDestinationPath,
    resolvedHooksConfig: {
      ...hooksConfig,
      hooks: resolvedHooks,
    },
  };
}

function previewInstallPlan(plan) {
  const migration = prepareClaudeSkillMigration(plan);
  return {
    ...plan,
    statePreview: migration.finalState,
    plannedOperations: [...plan.operations],

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Open the file at hooksSourcePath and verify the top level has "hooks": { ... } (object).
  2. Restore the file from git: git checkout -- hooks/hooks.json.
  3. Validate against Claude Code's hooks schema before re-running.
  4. Confirm no pre-processor (e.g. placeholder substitution) is returning undefined when pluginRoot is empty.

Example fix

// before (hooks/hooks.json)
{
  "hooks": [
    { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo hi" } ] }
  ]
}

// after
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo hi" } ] }
    ]
  }
}
Defensive patterns

Strategy: type-guard

Validate before calling

const cfg = JSON.parse(fs.readFileSync('hooks/hooks.json', 'utf8'));
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
  throw new Error('hooks.json top level must be an object');
}
if (!cfg.hooks || typeof cfg.hooks !== 'object' || Array.isArray(cfg.hooks)) {
  throw new Error('hooks.json must have a "hooks" object');
}

Type guard

function isHooksConfig(value) {
  return Boolean(
    value && typeof value === 'object' && !Array.isArray(value)
    && value.hooks && typeof value.hooks === 'object' && !Array.isArray(value.hooks)
  );
}

Prevention

When it happens

Trigger: ECC's bundled hooks/hooks.json (or a fork's equivalent) has its hooks field set to an array, null, or undefined after placeholder substitution; or a downstream fork restructured hooks as a list.

Common situations: Manual edit of the bundled hooks file; a bad merge that wrapped hooks in an array; a fork that switched to a list-based representation without updating the applier.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/fb375e6905ba4687. Report an issue: GitHub.