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
- Pass the actual plugin root directory string (e.g. the plugin install path)
- Resolve the root before calling: if from env, assert it is set and non-empty
- Check the call site argument order for materializeManagedHooks(hooksConfig, targetRoot)
- 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
- Resolve pluginRoot from config/env once, centrally, with an assertion
- Never pass possibly-undefined options directly into library calls
- Use path.resolve to normalize the root before use
- Add an argument-order test for materializeManagedHooks
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
- fps must be positive, got
- image_to_3d needs at least one image
- image_to_3d needs at least one image
- Invalid Claude target root: expected a non-empty string
- invalid issueNumber: expected positive integer, got
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)