{"record":{"id":"45f8193c45a518a4","repo":"Yeachan-Heo/oh-my-codex","slug":"state-key-is-required","errorCode":null,"errorMessage":"state key is required","messagePattern":"state key is required","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/hooks/extensibility/sdk/plugin-state.ts","lineNumber":18,"sourceCode":"import { existsSync } from 'fs';\nimport { mkdir, readFile, unlink, writeFile } from 'fs/promises';\nimport { dirname, join } from 'path';\nimport type { HookPluginSdk } from '../types.js';\nimport { hookPluginDataPath, hookPluginRootDir, sanitizeHookPluginName } from './paths.js';\n\nasync function readJsonIfExists<T>(path: string, fallback: T): Promise<T> {\n  if (!existsSync(path)) return fallback;\n  try {\n    return JSON.parse(await readFile(path, 'utf-8')) as T;\n  } catch {\n    return fallback;\n  }\n}\n\nexport function normalizeHookPluginStateKey(key: string): string {\n  const trimmed = key.trim();\n  if (!trimmed) throw new Error('state key is required');\n  if (trimmed.includes('..') || trimmed.startsWith('/')) {\n    throw new Error('invalid state key');\n  }\n  return trimmed;\n}\n\nexport function createHookPluginStateApi(\n  cwd: string,\n  pluginName: string,\n): HookPluginSdk['state'] {\n  const dataPath = hookPluginDataPath(cwd, pluginName);\n\n  async function readData(): Promise<Record<string, unknown>> {\n    return readJsonIfExists<Record<string, unknown>>(dataPath, {});\n  }\n\n  async function writeData(value: Record<string, unknown>): Promise<void> {\n    await mkdir(dirname(dataPath), { recursive: true });","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/hooks/extensibility/sdk/plugin-state.ts#L1-L36","documentation":"Thrown by normalizeHookPluginStateKey when the provided plugin state key is empty or only whitespace after trimming. State keys address per-plugin persisted state, so an empty key is rejected as invalid input.","triggerScenarios":"Calling any hook plugin state API (get/set/update) with key = '', '   ', or a value that trims to empty; passing an unconfigured/undefined-then-defaulted-to-empty key variable from plugin config.","commonSituations":"Plugin config missing the stateKey field so it defaults to ''; dynamic key construction producing an empty string (e.g. `${prefix}${suffix}` with both empty); whitespace-padded keys from YAML/JSON config files.","solutions":["Provide a non-empty, trimmed state key (e.g. the plugin's name or ID) when initializing the plugin state API","Check plugin configuration for a missing or blank stateKey/name field and set it explicitly","Trim/validate dynamic keys before use and fall back to a stable default identifier"],"exampleFix":"// before\nconst state = createHookPluginStateApi(cwd, '');\n\n// after\nconst state = createHookPluginStateApi(cwd, 'my-plugin');","handlingStrategy":"validation","validationCode":"const key = (config.stateKey ?? config.name ?? '').trim();\nif (!key) throw new TypeError('Plugin state key missing: set stateKey or name in plugin config');","typeGuard":"function isValidStateKey(key: unknown): key is string {\n  return typeof key === 'string' && key.trim().length > 0 && !key.includes('..') && !key.trim().startsWith('/');\n}","tryCatchPattern":"try { state.get(key); } catch (err) {\n  if ((err as Error).message === 'state key is required') throw new TypeError(`Misconfigured plugin: missing state key`);\n  throw err;\n}","preventionTips":["Always derive the state key from a required plugin identifier at construction","Fail fast on empty keys in your plugin loader, not at first state access","Add a unit test asserting your plugin's key passes normalizeHookPluginStateKey"],"tags":["validation","plugin-state","hooks"],"backgroundTag":"missing-required-argument","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}