Files
EapilSkillMarket/plugins/codex/plugins/oh-my-codex/hooks/codex-native-hook.mjs
T
KeyInfo Bot 881cde2d24 Sync third-party and MCP marketplace plugins
Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
2026-07-30 00:04:11 +08:00

658 lines
23 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { extname } from 'node:path';
import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const hookDir = dirname(fileURLToPath(import.meta.url));
// sync-plugin-mirror verifies this stable marker; runtime behavior is tested separately.
const OMX_PLUGIN_HOOK_LAUNCHER_CONTRACT_MARKER = 'omx-plugin-hook-launcher:v1';
const OMX_PLUGIN_HOOK_ROUTING_ONLY_MARKER = 'omx-plugin-hook-routing-only:v1';
const MAX_WRAPPER_STDIN_BYTES = 1024 * 1024;
const RAW_EVENT_SCAN_BYTES = 64 * 1024;
const MAX_STOP_STDOUT_BYTES = 1024 * 1024;
const MAX_ROUTING_RECORD_BYTES = 4 * 1024;
const CODEX_HOOK_EVENT_NAMES = new Set([
'SessionStart',
'PreToolUse',
'PostToolUse',
'UserPromptSubmit',
'PreCompact',
'PostCompact',
'Stop',
]);
function skipJsonWhitespace(raw, index) {
while (index < raw.length && /\s/.test(raw[index] ?? '')) index += 1;
return index;
}
function readJsonStringLiteral(raw, quoteIndex) {
if (raw[quoteIndex] !== '"') return null;
let value = '';
for (let index = quoteIndex + 1; index < raw.length; index += 1) {
const char = raw[index];
if (char === '"') return { value, endIndex: index + 1 };
if (char !== '\\') {
value += char;
continue;
}
index += 1;
if (index >= raw.length) return null;
const escaped = raw[index];
switch (escaped) {
case '"':
case '\\':
case '/':
value += escaped;
break;
case 'b':
value += '\b';
break;
case 'f':
value += '\f';
break;
case 'n':
value += '\n';
break;
case 'r':
value += '\r';
break;
case 't':
value += '\t';
break;
case 'u': {
const hex = raw.slice(index + 1, index + 5);
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return null;
value += String.fromCharCode(Number.parseInt(hex, 16));
index += 4;
break;
}
default:
return null;
}
}
return null;
}
function extractTopLevelStringField(rawInput, fieldNames) {
const raw = rawInput.slice(0, RAW_EVENT_SCAN_BYTES);
const wanted = new Set(fieldNames);
let depth = 0;
let index = 0;
while (index < raw.length) {
const char = raw[index];
if (char === '"') {
const key = readJsonStringLiteral(raw, index);
if (!key) return null;
index = key.endIndex;
const afterKey = skipJsonWhitespace(raw, index);
if (depth === 1 && raw[afterKey] === ':' && wanted.has(key.value)) {
const valueStart = skipJsonWhitespace(raw, afterKey + 1);
const value = readJsonStringLiteral(raw, valueStart);
return value?.value ?? null;
}
continue;
}
if (char === '{') depth += 1;
else if (char === '}') depth = Math.max(0, depth - 1);
index += 1;
}
return null;
}
function extractTopLevelHookEventName(rawInput) {
const eventName = extractTopLevelStringField(rawInput, ['hook_event_name', 'hookEventName', 'event', 'name']);
return CODEX_HOOK_EVENT_NAMES.has(eventName) ? eventName : null;
}
function detectStopHookInput(input) {
const text = input.toString('utf8');
try {
const parsed = JSON.parse(text);
const eventName = parsed?.hook_event_name ?? parsed?.hookEventName ?? parsed?.event ?? parsed?.name;
return eventName === 'Stop';
} catch {
return extractTopLevelHookEventName(text) === 'Stop';
}
}
function detectCompactHookInput(input) {
const text = input.toString('utf8');
try {
const parsed = JSON.parse(text);
const eventName = parsed?.hook_event_name ?? parsed?.hookEventName ?? parsed?.event ?? parsed?.name;
return eventName === 'PreCompact' || eventName === 'PostCompact';
} catch {
const eventName = extractTopLevelHookEventName(text);
return eventName === 'PreCompact' || eventName === 'PostCompact';
}
}
function parseHookPayload(input) {
try {
const parsed = JSON.parse(input.toString('utf8'));
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
function sanitizeLaunchId(value) {
return String(value ?? '').trim().replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128);
}
function resolveLaunchRoutingPath(payload, launchId) {
const cwd = typeof payload.cwd === 'string' && payload.cwd.trim() ? payload.cwd : process.cwd();
const stateRoot = typeof process.env.OMX_ROOT === 'string' && process.env.OMX_ROOT.trim()
? process.env.OMX_ROOT.trim()
: join(cwd, '.omx');
return join(stateRoot, 'state', 'plugin-hook-routing', `${sanitizeLaunchId(launchId)}.json`);
}
function hookPayloadSessionId(input, payload) {
const candidate = payload?.session_id ?? payload?.sessionId;
const parsedSessionId = typeof candidate === 'string' ? candidate.trim() : '';
if (parsedSessionId) return parsedSessionId;
return input && typeof input.toString === 'function'
? extractTopLevelStringField(input.toString('utf8'), ['session_id', 'sessionId'])?.trim() ?? ''
: '';
}
function isSafeRoutingSessionId(value) {
return typeof value === 'string' && value.length > 0 && value.length <= 256;
}
function isChildRoutingSessionStart(payload, ownerSessionId) {
if ((payload?.hook_event_name ?? payload?.hookEventName) !== 'SessionStart' || !ownerSessionId) return false;
const childSessionId = hookPayloadSessionId(null, payload);
const transcriptCandidate = payload?.transcript_path ?? payload?.transcriptPath;
const transcriptPath = typeof transcriptCandidate === 'string' ? transcriptCandidate.trim() : '';
if (!childSessionId || !transcriptPath) return false;
let fd;
try {
fd = openSync(transcriptPath, 'r');
const buffer = Buffer.alloc(64 * 1024);
const bytes = readSync(fd, buffer, 0, buffer.length, 0);
const newline = buffer.subarray(0, bytes).indexOf(0x0a);
const firstLine = buffer.subarray(0, newline >= 0 ? newline : bytes).toString('utf8').trim();
const record = JSON.parse(firstLine);
const metadata = record?.type === 'session_meta' && record?.payload && typeof record.payload === 'object' ? record.payload : null;
const spawn = metadata?.source?.subagent?.thread_spawn;
const metadataIds = [metadata?.id, metadata?.session_id].filter((value) => value !== undefined);
return metadataIds.length > 0
&& metadataIds.every((value) => value === childSessionId)
&& spawn && typeof spawn === 'object'
&& spawn.parent_thread_id === ownerSessionId;
} catch {
return false;
} finally {
if (fd !== undefined) try { closeSync(fd); } catch {}
}
}
function readPinnedRoutingRecord(routingPath) {
let fd;
try {
const initial = lstatSync(routingPath);
if (!initial.isFile() || initial.isSymbolicLink() || initial.nlink !== 1 || initial.size <= 0 || initial.size > MAX_ROUTING_RECORD_BYTES) return null;
fd = openSync(routingPath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
const opened = fstatSync(fd);
if (!opened.isFile() || opened.nlink !== 1 || opened.size !== initial.size || opened.dev !== initial.dev || opened.ino !== initial.ino) return null;
const buffer = Buffer.alloc(opened.size);
if (readSync(fd, buffer, 0, buffer.length, 0) !== buffer.length) return null;
const current = lstatSync(routingPath);
const final = fstatSync(fd);
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1 || current.size !== opened.size || current.dev !== opened.dev || current.ino !== opened.ino || final.nlink !== 1 || final.size !== opened.size) return null;
const routing = JSON.parse(buffer.toString('utf8'));
return routing && typeof routing === 'object' && !Array.isArray(routing) ? routing : null;
} catch {
return null;
} finally {
if (fd !== undefined) try { closeSync(fd); } catch {}
}
}
// This is routing correlation only. It is intentionally unauthenticated and must
// never be used as authorization or proof that a session is OMX-owned.
function isPluginHookRoutingSession(input, payload) {
const launchId = sanitizeLaunchId(process.env.OMX_CODEX_LAUNCH_ID);
const entryPath = process.env.OMX_ENTRY_PATH?.trim();
const sessionId = hookPayloadSessionId(input, payload);
if (!launchId || !entryPath || !isSafeRoutingSessionId(sessionId)) return false;
const routingPath = resolveLaunchRoutingPath(payload, launchId);
try {
if (existsSync(routingPath)) {
const routing = readPinnedRoutingRecord(routingPath);
if (!routing) return false;
const ownerSessionId = routing.routing === OMX_PLUGIN_HOOK_ROUTING_ONLY_MARKER && isSafeRoutingSessionId(routing.ownerSessionId)
? routing.ownerSessionId
: '';
return ownerSessionId === sessionId || isChildRoutingSessionStart(payload, ownerSessionId);
}
mkdirSync(dirname(routingPath), { recursive: true });
writeFileSync(routingPath, `${JSON.stringify({ routing: OMX_PLUGIN_HOOK_ROUTING_ONLY_MARKER, ownerSessionId: sessionId })}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
return true;
} catch {
return false;
}
}
function writePlainCodexNoop(isStop) {
if (isStop) process.stdout.write('{}\n');
process.exitCode = 0;
}
async function readBoundedStdin({ drainOversized = false } = {}) {
const chunks = [];
let totalBytes = 0;
let storedBytes = 0;
let oversized = false;
for await (const rawChunk of process.stdin) {
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
totalBytes += chunk.length;
if (oversized) continue;
const remaining = MAX_WRAPPER_STDIN_BYTES - storedBytes;
if (chunk.length > remaining) {
if (remaining > 0) chunks.push(chunk.subarray(0, remaining));
storedBytes += Math.max(remaining, 0);
oversized = true;
if (!drainOversized) return { input: Buffer.concat(chunks), oversized: true, totalBytes };
continue;
}
chunks.push(chunk);
storedBytes += chunk.length;
}
return { input: Buffer.concat(chunks), oversized, totalBytes };
}
function stopFallbackOutput(stopReason, detail) {
const reason = 'OMX plugin Stop hook launcher failed before valid native Stop JSON could be produced. Continue once, preserve runtime state, inspect hook launcher diagnostics, and retry.';
return {
decision: 'block',
reason,
stopReason,
systemMessage: detail ? `${reason} Failure: ${detail}` : reason,
};
}
function writeStopFallback(stopReason, detail) {
process.stdout.write(`${JSON.stringify(stopFallbackOutput(stopReason, detail))}\n`);
process.exitCode = 0;
}
function writeOversizedStopFallback(stopReason, detail) {
const reason =
'OMX plugin Stop rejected oversized stdin before launcher delegation; continue once with a smaller valid Stop JSON response.';
process.stdout.write(`${JSON.stringify({
decision: 'block',
reason,
stopReason,
systemMessage: detail ? `${reason} Failure: ${detail}` : reason,
})}\n`);
process.exitCode = 0;
}
function writeOversizedStopNoop() {
process.stdout.write('{}\n');
process.exitCode = 0;
}
function writeOversizedToolHookOutput(eventName) {
const systemMessage = 'OMX native hook rejected oversized stdin JSON before parsing; maxBytes=1048576.';
const output = eventName === 'PreToolUse'
? {
systemMessage,
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: systemMessage,
},
}
: {
continue: false,
stopReason: 'native_hook_stdin_oversized',
systemMessage,
};
process.stdout.write(`${JSON.stringify(output)}\n`);
process.exitCode = 0;
}
function writeCompactFallback() {
process.exitCode = 0;
}
function failLauncher(error, isStop, isCompact, stopReason = 'plugin_stop_hook_launcher_failure') {
const detail = error instanceof Error ? error.message : String(error);
console.error(`[oh-my-codex] ${detail}`);
if (isStop) {
writeStopFallback(stopReason, detail);
return;
}
if (isCompact) {
writeCompactFallback();
return;
}
process.exitCode = 1;
}
function readPinnedLauncher() {
const launcherPath = join(hookDir, 'omx-command.json');
try {
const raw = JSON.parse(readFileSync(launcherPath, 'utf8'));
if (typeof raw.command !== 'string' || raw.command.trim() === '') {
throw new Error('missing non-empty command');
}
const argsPrefix = Array.isArray(raw.argsPrefix) ? raw.argsPrefix : [];
if (!argsPrefix.every((arg) => typeof arg === 'string')) {
throw new Error('argsPrefix must contain only strings');
}
return { command: raw.command, argsPrefix };
} catch (error) {
if (error?.code === 'ENOENT') return null;
throw new Error(`invalid plugin hook launcher ${launcherPath}: ${error.message}`);
}
}
function readConfiguredLauncher() {
if (process.env.OMX_NATIVE_HOOK_COMMAND) {
return { command: process.env.OMX_NATIVE_HOOK_COMMAND, argsPrefix: [] };
}
return readPinnedLauncher() ?? { command: 'omx', argsPrefix: [] };
}
function buildSpawnOptions(command) {
const options = {
stdio: ['pipe', 'pipe', 'pipe'],
env: process.env,
};
if (process.platform !== 'win32') return options;
const extension = extname(command).toLowerCase();
if (extension === '.exe' || extension === '.com') {
return { ...options, windowsHide: true };
}
return { ...options, shell: true, windowsHide: true };
}
function readJsonFile(path) {
try {
return JSON.parse(readFileSync(path, 'utf8'));
} catch {
return null;
}
}
function isTerminalOutcome(value) {
return ['finish', 'finished', 'complete', 'completed', 'done', 'blocked', 'blocked-on-user', 'blocked_on_user', 'failed', 'fail', 'error', 'cancelled', 'canceled', 'cancel', 'aborted', 'abort', 'userinterlude', 'user-interlude', 'interrupted', 'interrupt', 'askuserquestion', 'ask-user-question', 'askuser', 'question'].includes(String(value ?? '').trim().toLowerCase());
}
function isTerminalRunStateForMode(state, mode) {
if (!state) return false;
const runMode = String(state.mode ?? '').trim();
if (runMode && runMode !== mode) return false;
return isTerminalOutcome(state.outcome)
|| isTerminalOutcome(state.run_outcome)
|| isTerminalOutcome(state.lifecycle_outcome)
|| isTerminalOutcome(state.terminal_outcome);
}
function canonicalPath(path) {
const absolute = resolve(path);
if (!existsSync(absolute)) return absolute;
try {
return typeof realpathSync.native === 'function' ? realpathSync.native(absolute) : realpathSync(absolute);
} catch {
return absolute;
}
}
function sameFilePath(leftPath, rightPath) {
return canonicalPath(leftPath) === canonicalPath(rightPath);
}
function isSessionStateAuthoritativeForCwd(state, cwd) {
if (!isSafeSessionId(state?.session_id)) return false;
const sessionCwd = typeof state.cwd === 'string' ? state.cwd.trim() : '';
return !sessionCwd || sameFilePath(sessionCwd, cwd);
}
function listAuthoritativeStateBaseDirs(cwd) {
if (process.env.OMX_TEAM_STATE_ROOT?.trim()) return [process.env.OMX_TEAM_STATE_ROOT.trim()];
if (process.env.OMX_ROOT?.trim()) return [join(process.env.OMX_ROOT.trim(), '.omx', 'state')];
if (process.env.OMX_STATE_ROOT?.trim()) return [join(process.env.OMX_STATE_ROOT.trim(), '.omx', 'state')];
return [join(cwd, '.omx', 'state')];
}
function isSafeSessionId(sessionId) {
return typeof sessionId === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(sessionId.trim());
}
function readCurrentSessionId(stateBaseDirs, cwd) {
for (const stateDir of stateBaseDirs) {
const session = readJsonFile(join(stateDir, 'session.json'));
if (isSessionStateAuthoritativeForCwd(session, cwd)) return session.session_id.trim();
}
const envSessionId = process.env.OMX_SESSION_ID || process.env.CODEX_SESSION_ID;
return isSafeSessionId(envSessionId) ? envSessionId.trim() : null;
}
function shouldContinueAutopilotState(state) {
if (state?.active !== true) return false;
return !(isTerminalOutcome(state.current_phase)
|| isTerminalOutcome(state.run_outcome)
|| isTerminalOutcome(state.lifecycle_outcome)
|| isTerminalOutcome(state.terminal_outcome)
|| isTerminalOutcome(state.outcome)
|| (typeof state.completed_at === 'string' && state.completed_at.trim() !== ''));
}
function hasActiveAutopilotStateForOversizedStop(input) {
const text = input.toString('utf8');
const cwd = extractTopLevelStringField(text, ['cwd']) || process.cwd();
const stateBaseDirs = listAuthoritativeStateBaseDirs(cwd);
const sessionId = readCurrentSessionId(stateBaseDirs, cwd);
if (!isSafeSessionId(sessionId)) return false;
const sessionDir = join(stateBaseDirs[0], 'sessions', sessionId.trim());
const terminalRunState = readJsonFile(join(sessionDir, 'run-state.json'));
if (isTerminalRunStateForMode(terminalRunState, 'autopilot')) return false;
const sessionState = readJsonFile(join(sessionDir, 'autopilot-state.json'));
return shouldContinueAutopilotState(sessionState);
}
function parseJsonObjectCandidate(text) {
try {
const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
return parsed;
} catch {
return null;
}
}
function isJsonObjectFragmentLine(text) {
return text.startsWith('{') || text.endsWith('}') || /^"[^"]+"\s*:/.test(text);
}
function parseSingleJsonObjectOutput(raw) {
const text = String(raw ?? '').trim();
if (!text) return null;
const direct = parseJsonObjectCandidate(text);
if (direct) return direct;
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
const lastLine = lines.at(-1);
if (!lastLine || !lastLine.startsWith('{') || !lastLine.endsWith('}')) return null;
for (const line of lines.slice(0, -1)) {
if (parseJsonObjectCandidate(line) || isJsonObjectFragmentLine(line)) return null;
}
return parseJsonObjectCandidate(lastLine);
}
async function main() {
const { input, oversized, totalBytes } = await readBoundedStdin({ drainOversized: true });
const payload = parseHookPayload(input);
const routedByLaunchCorrelation = isPluginHookRoutingSession(input, payload);
const isStop = detectStopHookInput(input);
const isCompact = detectCompactHookInput(input);
if (!routedByLaunchCorrelation) {
writePlainCodexNoop(isStop);
return;
}
if (oversized) {
const message = `plugin hook stdin exceeded ${MAX_WRAPPER_STDIN_BYTES} bytes before launcher delegation; totalBytes>${totalBytes}`;
if (isStop) {
if (hasActiveAutopilotStateForOversizedStop(input)) {
console.error(`[oh-my-codex] ${message}`);
writeOversizedStopFallback('plugin_stop_hook_stdin_oversized_active_workflow', message);
return;
}
writeOversizedStopNoop();
return;
}
const eventName = extractTopLevelHookEventName(input.toString('utf8'));
if (eventName === 'PreToolUse' || eventName === 'PostToolUse') {
writeOversizedToolHookOutput(eventName);
return;
}
console.error(`[oh-my-codex] ${message}`);
process.exitCode = 1;
return;
}
let launcher;
try {
launcher = readConfiguredLauncher();
} catch (error) {
failLauncher(error, isStop, isCompact);
return;
}
const { command, argsPrefix } = launcher;
const child = spawn(command, [...argsPrefix, 'codex-native-hook'], buildSpawnOptions(command));
const stdoutChunks = [];
let stdoutBytes = 0;
let bufferedStopStdoutBytes = 0;
let stopStdoutOversized = false;
let childSpawnError = null;
let childStdinError = null;
child.stdout.on('data', (chunk) => {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
stdoutBytes += buffer.byteLength;
if (isStop) {
if (!stopStdoutOversized) {
const remaining = MAX_STOP_STDOUT_BYTES - bufferedStopStdoutBytes;
if (remaining > 0) {
const slice = buffer.subarray(0, remaining);
stdoutChunks.push(slice);
bufferedStopStdoutBytes += slice.byteLength;
}
if (buffer.byteLength > remaining) {
stopStdoutOversized = true;
child.stdout.destroy();
child.kill();
}
}
} else if (!isCompact) {
process.stdout.write(chunk);
}
});
child.stderr.pipe(process.stderr);
child.stdin.on('error', (error) => {
childStdinError = error;
});
child.on('error', (error) => {
childSpawnError = error;
});
child.on('close', (code, signal) => {
if (isStop && stopStdoutOversized) {
writeStopFallback('plugin_stop_hook_launcher_stdout_oversized', `codex-native-hook produced more than ${MAX_STOP_STDOUT_BYTES} bytes of Stop hook stdout`);
return;
}
if (isStop && stdoutBytes > 0) {
const stdoutText = Buffer.concat(stdoutChunks).toString('utf8');
const parsed = parseSingleJsonObjectOutput(stdoutText);
if (parsed) {
process.stdout.write(`${JSON.stringify(parsed)}\n`);
process.exitCode = 0;
return;
}
writeStopFallback('plugin_stop_hook_launcher_invalid_stdout', `codex-native-hook produced invalid Stop hook JSON stdout (${stdoutBytes} bytes)`);
return;
}
if (isCompact) {
process.exitCode = 0;
return;
}
if (isStop && stdoutBytes === 0) {
if (childSpawnError) {
writeStopFallback('plugin_stop_hook_launcher_spawn_error', `failed to launch ${command} codex-native-hook: ${childSpawnError.message}`);
return;
}
if (signal) {
writeStopFallback('plugin_stop_hook_launcher_signal', `codex-native-hook terminated by ${signal}`);
return;
}
if (code && code !== 0) {
if (childStdinError) {
writeStopFallback('plugin_stop_hook_launcher_stdin_error', `codex-native-hook stdin failed: ${childStdinError.message}`);
return;
}
writeStopFallback('plugin_stop_hook_launcher_exit', `codex-native-hook exited with code ${code}`);
return;
}
writeStopFallback('plugin_stop_hook_launcher_empty_stdout', 'codex-native-hook exited successfully without producing Stop hook JSON');
return;
}
if (isCompact) {
if (childSpawnError) {
console.error(`[oh-my-codex] failed to launch ${command} codex-native-hook: ${childSpawnError.message}`);
} else if (signal) {
console.error(`[oh-my-codex] codex-native-hook terminated by ${signal}`);
} else if (code && code !== 0) {
console.error(`[oh-my-codex] codex-native-hook exited with code ${code}`);
}
process.exitCode = 0;
return;
}
if (childSpawnError) {
console.error(`[oh-my-codex] failed to launch ${command} codex-native-hook: ${childSpawnError.message}`);
process.exitCode = 1;
return;
}
if (signal) {
console.error(`[oh-my-codex] codex-native-hook terminated by ${signal}`);
process.exitCode = 1;
return;
}
process.exitCode = code ?? 0;
});
child.stdin.end(input);
}
main().catch((error) => {
console.error(`[oh-my-codex] plugin hook launcher failed: ${error instanceof Error ? error.message : String(error)}`);
process.exitCode = 1;
});