affaan-m/ECC · error

Invalid Claude plugin root: expected a string

Error message

Invalid Claude plugin root: expected a string

What it means

replacePluginRootPlaceholders substitutes the CLAUDE_PLUGIN_ROOT placeholder in hook commands with a concrete plugin root path. The function requires that root as a non-empty-ish string; when pluginRoot is not a string (undefined, null, number), it throws immediately rather than producing commands with an invalid root baked in.

Solutions

  1. Pass the actual plugin root directory string (e.g. the plugin install path)
  2. Resolve the root before calling: if from env, assert it is set and non-empty
  3. Check the call site argument order for materializeManagedHooks(hooksConfig, targetRoot)
  4. Default the root explicitly (e.g. process.cwd()) when appropriate

Example fix

// before
replacePluginRootPlaceholders(hooks, process.env.PLUGIN_ROOT)
// after
const root = process.env.PLUGIN_ROOT;
if (typeof root !== 'string' || root.length === 0) throw new Error('PLUGIN_ROOT is required');
replacePluginRootPlaceholders(hooks, root)
Defensive patterns

Strategy: validation

Validate before calling

if (typeof pluginRoot !== 'string' || pluginRoot.length === 0) throw new Error('pluginRoot must be a non-empty string');

Type guard

const isNonEmptyString = (v) => typeof v === 'string' && v.trim().length > 0;

Try / catch

try { return replacePluginRootPlaceholders(value, root); } catch (e) { if (e.message.includes('plugin root')) { throw new Error('PLUGIN_ROOT not configured: ' + e.message); } throw e; }

Prevention

When it happens

Trigger: Calling replacePluginRootPlaceholders (directly or via materializeManagedHooks/resolved) with pluginRoot = undefined/null/non-string — e.g. a missing config value passed straight through.

Common situations: An install script reads the plugin root from an unset option or env var, a refactor changed the parameter order, or targetRoot was omitted when calling materializeManagedHooks.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

  }

  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);
  }
  if (Array.isArray(value)) {
    return value.map(item => replacePluginRootPlaceholders(item, pluginRoot));
  }
  if (isJsonObject(value)) {
    return Object.fromEntries(
      Object.entries(value).map(([key, nestedValue]) => [
        key,
        replacePluginRootPlaceholders(nestedValue, pluginRoot),
      ])
    );
  }
  return value;
}

View on GitHub (pinned to 8321021c54)