{"record":{"id":"0b6626bbeb974f06","repo":"thedotmack/claude-mem","slug":"plugin-hooks-codex-hooks-json-contains-unsupported","errorCode":null,"errorMessage":"plugin/hooks/codex-hooks.json contains unsupported Codex root key: ${rootKey}","messagePattern":"plugin/hooks/codex-hooks\\.json contains unsupported Codex root key: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"scripts/build-hooks.js","lineNumber":730,"sourceCode":"      'plugin/scripts/bun-runner.js',\n      'plugin/sqlite/SessionStore.js',\n      'plugin/sqlite/observations/files.js',\n      'plugin/.claude-plugin/plugin.json',\n      'plugin/.codex-plugin/plugin.json',\n      'plugin/.mcp.json',\n      '.codex-plugin/plugin.json',\n      '.agents/plugins/marketplace.json',\n    ];\n    for (const filePath of requiredDistributionFiles) {\n      if (!fs.existsSync(filePath)) {\n        throw new Error(`Missing required distribution file: ${filePath}`);\n      }\n    }\n    const codexHooks = JSON.parse(fs.readFileSync('plugin/hooks/codex-hooks.json', 'utf-8'));\n    const validCodexHookRootKeys = new Set(['hooks']);\n    for (const rootKey of Object.keys(codexHooks)) {\n      if (!validCodexHookRootKeys.has(rootKey)) {\n        throw new Error(`plugin/hooks/codex-hooks.json contains unsupported Codex root key: ${rootKey}`);\n      }\n    }\n    for (const eventName of Object.keys(codexHooks.hooks ?? {})) {\n      if (!validCodexHookEvents.has(eventName)) {\n        throw new Error(`plugin/hooks/codex-hooks.json contains unknown Codex hook event: ${eventName}`);\n      }\n    }\n    const codexMarketplace = JSON.parse(fs.readFileSync('.agents/plugins/marketplace.json', 'utf-8'));\n    const claudeMemMarketplaceEntry = (codexMarketplace.plugins ?? []).find((plugin) => plugin.name === 'claude-mem');\n    if (claudeMemMarketplaceEntry?.source?.path !== './plugin') {\n      throw new Error('.agents/plugins/marketplace.json must point claude-mem source.path at ./plugin so Codex loads the bundled plugin root');\n    }\n    const bundledMcp = JSON.parse(fs.readFileSync('plugin/.mcp.json', 'utf-8'));\n    const mcpSearchCommand = bundledMcp.mcpServers?.['mcp-search']?.args?.join(' ') ?? '';\n    if (!mcpSearchCommand.includes('.codex/plugins/cache/claude-mem-local/claude-mem')) {\n      throw new Error('plugin/.mcp.json mcp-search launcher must include Codex cache fallback for hosts that do not inject PLUGIN_ROOT');\n    }\n    if (!mcpSearchCommand.includes('plugins/cache/thedotmack/claude-mem')) {","sourceCodeStart":712,"sourceCodeEnd":748,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/scripts/build-hooks.js#L712-L748","documentation":"After loading plugin/hooks/codex-hooks.json, the build verifies every top-level key is in the allowed set {hooks}. Codex ignores unknown root keys (or errors opaquely), so any extraneous key would silently break the plugin under Codex. The guard names the offending key.","triggerScenarios":"Editing plugin/hooks/codex-hooks.json and adding a top-level field (e.g. a copied 'events', 'config', or Claude-specific key) that Codex doesn't recognise. Cross-pasting from a Claude hooks JSON that has a different schema.","commonSituations":"Treating the Codex hooks file like the Claude settings.json schema. Adding metadata fields at the root. Merge that brings in a sibling key from another hook system.","solutions":["Open plugin/hooks/codex-hooks.json and remove or nest the named root key — only 'hooks' may sit at the top level.","If you need metadata, put it under a Codex-supported nested location or a separate sidecar file, not a new root key.","Re-run the build; the check at scripts/build-hooks.js:728-731 must pass before downstream event validation."],"exampleFix":"// before: plugin/hooks/codex-hooks.json\n{\n  \"hooks\": { ... },\n  \"events\": { \"SessionStart\": [...] }   // unsupported root key\n}\n\n// after — only 'hooks' at the root\n{\n  \"hooks\": {\n    \"SessionStart\": [...]   // moved under hooks\n  }\n}","handlingStrategy":"validation","validationCode":"const codexHooks = JSON.parse(fs.readFileSync('plugin/hooks/codex-hooks.json','utf8'));\nconst badRoots = Object.keys(codexHooks).filter(k => k !== 'hooks');\nif (badRoots.length) throw new Error('Unsupported codex root keys: ' + badRoots.join(','));","typeGuard":"function isCodexHooksRoot(obj): obj is { hooks: Record<string, unknown> } {\n  return obj && typeof obj === 'object' && 'hooks' in obj && Object.keys(obj).every(k => k === 'hooks');\n}","tryCatchPattern":"// Build-time only. Remove the unsupported root key (or nest it under 'hooks').\n// Do not suppress — Codex would silently ignore the unknown key at runtime.","preventionTips":["Treat plugin/hooks/codex-hooks.json as schema-locked: only a top-level 'hooks' object.","Add a JSON-schema for codex-hooks.json and validate in CI.","Don't copy fields from the Claude settings.json schema into the Codex file."],"tags":["build","verification","codex","hooks","schema"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}