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

  1. Provide a non-empty, trimmed state key (e.g. the plugin's name or ID) when initializing the plugin state API
  2. Check plugin configuration for a missing or blank stateKey/name field and set it explicitly
  3. 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

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


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/45f8193c45a518a4. Report an issue: GitHub.