Yeachan-Heo/oh-my-codex · error · Error
state key is required
Error message
state key is required
What it means
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.
Source
Thrown at src/hooks/extensibility/sdk/plugin-state.ts:18
import { existsSync } from 'fs';
import { mkdir, readFile, unlink, writeFile } from 'fs/promises';
import { dirname, join } from 'path';
import type { HookPluginSdk } from '../types.js';
import { hookPluginDataPath, hookPluginRootDir, sanitizeHookPluginName } from './paths.js';
async function readJsonIfExists<T>(path: string, fallback: T): Promise<T> {
if (!existsSync(path)) return fallback;
try {
return JSON.parse(await readFile(path, 'utf-8')) as T;
} catch {
return fallback;
}
}
export function normalizeHookPluginStateKey(key: string): string {
const trimmed = key.trim();
if (!trimmed) throw new Error('state key is required');
if (trimmed.includes('..') || trimmed.startsWith('/')) {
throw new Error('invalid state key');
}
return trimmed;
}
export function createHookPluginStateApi(
cwd: string,
pluginName: string,
): HookPluginSdk['state'] {
const dataPath = hookPluginDataPath(cwd, pluginName);
async function readData(): Promise<Record<string, unknown>> {
return readJsonIfExists<Record<string, unknown>>(dataPath, {});
}
async function writeData(value: Record<string, unknown>): Promise<void> {
await mkdir(dirname(dataPath), { recursive: true });View on GitHub (pinned to 3ad79a8a6f)
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
Example fix
// before const state = createHookPluginStateApi(cwd, ''); // after const state = createHookPluginStateApi(cwd, 'my-plugin');
Defensive patterns
Strategy: validation
Validate before calling
const key = (config.stateKey ?? config.name ?? '').trim();
if (!key) throw new TypeError('Plugin state key missing: set stateKey or name in plugin config'); Type guard
function isValidStateKey(key: unknown): key is string {
return typeof key === 'string' && key.trim().length > 0 && !key.includes('..') && !key.trim().startsWith('/');
} Try / catch
try { state.get(key); } catch (err) {
if ((err as Error).message === 'state key is required') throw new TypeError(`Misconfigured plugin: missing state key`);
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- Native hook transaction wrote invalid hooks.json: ${validati
- invalid state key
- invalid auth slot path
- Autoresearch goal ${mission.slug} cannot complete until prof
- autoresearch candidate artifact notes must be a string array
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/45f8193c45a518a4.
Report an issue: GitHub.