affaan-m/ECC · error · Error
Missing Codex PLUGIN_ROOT
Error message
Missing Codex PLUGIN_ROOT
What it means
The Codex SessionStart hook bootstrap refuses to run when the PLUGIN_ROOT environment variable is not set. Codex does not expose the plugin directory the way Claude Code does (CLAUDE_PLUGIN_ROOT), so this wrapper requires the caller (Codex host or installer) to inject PLUGIN_ROOT explicitly, then forwards it as CLAUDE_PLUGIN_ROOT for the underlying bootstrap script. Without it the hook exits immediately with this Error, so session-start context loading and package-manager detection never run.
Solutions
- Set PLUGIN_ROOT to the absolute path of the ECC plugin root before Codex launches the hook, e.g. export PLUGIN_ROOT="$HOME/.claude/plugins/ecc" in your shell profile or Codex env config.
- Re-run the ECC install script for the Codex harness so it wires PLUGIN_ROOT into the generated hooks config.
- Sanity-check the value: PLUGIN_ROOT must point at the directory containing scripts/hooks/plugin-hook-bootstrap.js.
- As a diagnostic, run the node command manually with PLUGIN_ROOT set to confirm the bootstrap resolves.
Example fix
// before (Codex config, no env)
"command": "node -e \"if(!process.env.PLUGIN_ROOT)throw new Error('Missing Codex PLUGIN_ROOT');...\""
// after
"command": "PLUGIN_ROOT=/home/me/.claude/plugins/ecc node -e \"if(!process.env.PLUGIN_ROOT)throw new Error('Missing Codex PLUGIN_ROOT');...\"" Defensive patterns
Strategy: validation
Validate before calling
function hasPluginRoot() {
return typeof process.env.PLUGIN_ROOT === 'string'
&& process.env.PLUGIN_ROOT.trim().length > 0
&& require('fs').existsSync(require('path').join(process.env.PLUGIN_ROOT.trim(), 'scripts/hooks/plugin-hook-bootstrap.js'));
}
if (!hasPluginRoot()) throw new Error('PLUGIN_ROOT must point to the ECC plugin root containing scripts/hooks'); Type guard
const isValidPluginRoot = (v) => typeof v === 'string' && v.trim().length > 0 && v.trim() !== '/';
Try / catch
try {
runSessionStartHook();
} catch (err) {
if (err.message.includes('Missing Codex PLUGIN_ROOT')) {
console.error('Set PLUGIN_ROOT to the ECC plugin directory before launching Codex.');
}
throw err;
} Prevention
- Export PLUGIN_ROOT in your shell profile or Codex env config after every ECC install/reinstall.
- Verify the path contains scripts/hooks/plugin-hook-bootstrap.js before launching.
- Prefer the official ECC installer for Codex, which wires the env var automatically.
- Add a smoke check in CI that runs the SessionStart command with PLUGIN_ROOT set.
When it happens
Trigger: Running the ECC plugin under Codex without PLUGIN_ROOT in the environment; invoking the SessionStart command from hooks/codex-hooks.json in a shell or config where the installer failed to export PLUGIN_ROOT; migrating a Claude Code install to Codex and forgetting the Codex-specific env var.
Common situations: Users install the ECC plugin via one harness and copy hooks config to the other; Codex config files that don't support env interpolation; CI or headless shells where plugin env vars are not propagated.
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
Related errors
- Claude settings contains multiple hooks for event
- [ecc-codex] WARNING
- [ecc-mcp] WARNING
- Invalid hook entry at
- Invalid hooks config: expected a JSON object with a hooks…
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/916e88f4b93a43ad.
Report an issue: GitHub.
Appendix: source
Thrown at hooks/codex-hooks.json:10
{
"description": "ECC native Codex hook: verified SessionStart bootstrap. Claude hook profiles remain separate.",
"hooks": {
"SessionStart": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": "node -e \"if(!process.env.PLUGIN_ROOT)throw new Error('Missing Codex PLUGIN_ROOT');process.env.CLAUDE_PLUGIN_ROOT=process.env.PLUGIN_ROOT;const p=require('path');const r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
}
],
"description": "Load previous context and detect package manager on new session",
"id": "session:start"
}
]
}
}
View on GitHub (pinned to 8321021c54)