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

  1. 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.
  2. Re-run the ECC install script for the Codex harness so it wires PLUGIN_ROOT into the generated hooks config.
  3. Sanity-check the value: PLUGIN_ROOT must point at the directory containing scripts/hooks/plugin-hook-bootstrap.js.
  4. 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

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


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)