affaan-m/ECC · error
Unable to resolve CLAUDE_PLUGIN_ROOT in a managed hook…
Error message
Unable to resolve CLAUDE_PLUGIN_ROOT in a managed hook command; the hooks.json command prologue no longer matches the expected form
What it means
Managed hook commands reference CLAUDE_PLUGIN_ROOT through an expected env-var prologue. After rewriting the prologue to an inline assignment, the resolver re-checks the command; if a CLAUDE_PLUGIN_ROOT reference still remains, the prologue did not match the expected form and the placeholder cannot be safely resolved, so it throws rather than emitting a command that would fail or leak an unresolved variable at hook runtime.
Solutions
- Restore the command to the standard prologue form the library expects (the exact PLUGIN_ROOT_ENV_PROLOGUE prefix followed by the command using the placeholder)
- Regenerate hooks.json from the plugin template instead of hand-editing the command
- Inline the absolute plugin path manually if you do not want placeholder resolution
- Check the plugin version for hooks.json format drift and update
Example fix
// before
"command": "node \"$CLAUDE_PLUGIN_ROOT/scripts/hook.js\""
// after
"command": "CLAUDE_PLUGIN_ROOT=\"${CLAUDE_PLUGIN_ROOT:-}" node \"${CLAUDE_PLUGIN_ROOT}/scripts/hook.js\"" — or restore the canonical placeholder command from hooks.json template Defensive patterns
Strategy: try-catch
Validate before calling
const usesPlaceholder = (cmd) => cmd.includes('CLAUDE_PLUGIN_ROOT');
// before materializing: every command containing CLAUDE_PLUGIN_ROOT must match the canonical prologue template — verify against the pristine hooks.json from the plugin. Type guard
null
Try / catch
try { materializeManagedHooks(cfg, root); } catch (e) { if (e.message.includes('Unable to resolve CLAUDE_PLUGIN_ROOT')) { console.error('hooks.json command deviates from the canonical prologue; restore the template command'); } throw e; } Prevention
- Never hand-edit the CLAUDE_PLUGIN_ROOT prologue in hook commands
- Regenerate hooks.json from the plugin template after edits
- Keep quoting/formatting of the prologue byte-identical to the template
- Diff local hooks.json against upstream on plugin upgrades
When it happens
Trigger: Calling resolveCommand (via resolveManagedHookCommands/materializeManagedHooks) with a command string containing CLAUDE_PLUGIN_ROOT but not in the exact expected prologue form — e.g. "echo $CLAUDE_PLUGIN_ROOT/x" with no prologue, quoted variants, or a modified prologue after hand-editing hooks.json.
Common situations: A hook command was hand-edited or reformatted (different quoting, missing the standard prefix), a plugin shipped hooks.json deviating from the template, or an older format after a library upgrade changed the expected prologue.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- Claude settings contains multiple hooks for event
- --enable-hooks and --no-hooks are mutually exclusive
- File path contains unsafe shell characters
- git commit failed
- Invalid Claude hooks preference
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/d14ea1d3ad83a4e6.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/install/claude-settings.js:270
])
);
}
return value;
}
function resolveManagedHookCommands(managedHooks, targetRoot) {
const encodedRoot = Buffer.from(targetRoot, 'utf8').toString('base64');
const rootExpression = `Buffer.from('${encodedRoot}','base64').toString('utf8')`;
const resolveCommand = command => {
// Leave invalid entries intact so managed-hook validation reports them.
if (typeof command !== 'string') {
return command;
}
const resolved = command
.split(PLUGIN_ROOT_ENV_PROLOGUE)
.join(`var e=${rootExpression};`);
if (PLUGIN_ROOT_ENV_READ.test(resolved)) {
throw new Error(
'Unable to resolve CLAUDE_PLUGIN_ROOT in a managed hook command; '
+ 'the hooks.json command prologue no longer matches the expected form'
);
}
return resolved;
};
return Object.fromEntries(
Object.entries(managedHooks).map(([event, entries]) => [
event,
entries.map(entry => ({
...entry,
hooks: entry.hooks.map(hook => ({
...hook,
...(typeof hook.command === 'string' || Array.isArray(hook.command)
? {
command: Array.isArray(hook.command)
? hook.command.map(resolveCommand)
: resolveCommand(hook.command),View on GitHub (pinned to 8321021c54)