Yeachan-Heo/oh-my-codex · error · Error
invalid state key
Error message
invalid state key
What it means
Thrown by normalizeHookPluginStateKey when the key contains '..' or starts with '/'. These patterns would allow path traversal out of the plugin's state directory, so they are rejected as a path-safety guard.
Source
Thrown at src/hooks/extensibility/sdk/plugin-state.ts:20
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 });
await writeFile(dataPath, JSON.stringify(value, null, 2));
}View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Sanitize keys: strip leading '/', replace '..' segments, or encode the raw identifier (e.g. base64/hex or slugify) before passing it as a state key
- Use flat, slug-like keys such as 'my-plugin-session-1' derived from trusted identifiers
- Validate external keys against /^[A-Za-z0-9._-]+$/ before using them as state keys
Example fix
// before const key = userInput; // '../../etc/passwd' state.get(key); // after const key = slugify(userInput); // 'etc-passwd' with dots/traversal removed state.get(key);
Defensive patterns
Strategy: validation
Validate before calling
import { normalizeHookPluginStateKey } from './plugin-state.js';
function safeStateKey(raw: string): string {
return normalizeHookPluginStateKey(raw.replace(/\.\./g, '').replace(/^\/+/, '').replace(/[^\w.-]+/g, '-'));
} Type guard
function isSafeStateKey(key: string): boolean {
const t = key.trim();
return t.length > 0 && !t.includes('..') && !t.startsWith('/') && /^[A-Za-z0-9._-]+$/.test(t);
} Try / catch
try { state.get(key); } catch (err) {
if ((err as Error).message === 'invalid state key') return state.get(sanitize(key));
throw err;
} Prevention
- Never build state keys from raw user input or file paths
- Slugify or encode external identifiers before using them as keys
- Add a regex allow-list check in one place (key builder) rather than at each call site
When it happens
Trigger: Calling plugin state APIs with a key like '../other-plugin', 'a/../b', '/absolute/path', or any key containing '..' after trimming. Keys are used to build state file paths, so traversal segments are forbidden.
Common situations: Deriving state keys from user input or file names without sanitization; using slash-prefixed keys copied from a path constant; keys built from external identifiers that legitimately contain '..' sequences.
Related errors
- Path traversal detected: path is outside the allowed directo
- [ask] invalid --agent-prompt role "${role}". Expected lowerc
- invalid detached leader parent environment
- run directory escapes the authorized runs root
- state directory escapes the authorized run directory
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/71aa022bd5e8cda2.
Report an issue: GitHub.