fix(index): wrap hook creation with safeCreateHook + add defensive optional chaining (#1559)

This commit is contained in:
YeonGyu-Kim 2026-02-07 13:33:02 +09:00
parent 7ede8e04f0
commit 050e6a2187

View File

@ -109,6 +109,7 @@ import {
injectServerAuthIntoClient, injectServerAuthIntoClient,
} from "./shared"; } from "./shared";
import { filterDisabledTools } from "./shared/disabled-tools"; import { filterDisabledTools } from "./shared/disabled-tools";
import { safeCreateHook } from "./shared/safe-create-hook";
import { loadPluginConfig } from "./plugin-config"; import { loadPluginConfig } from "./plugin-config";
import { createModelCacheState } from "./plugin-state"; import { createModelCacheState } from "./plugin-state";
import { createConfigHandler } from "./plugin-handlers"; import { createConfigHandler } from "./plugin-handlers";
@ -135,21 +136,22 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
agent_pane_min_width: pluginConfig.tmux?.agent_pane_min_width ?? 40, agent_pane_min_width: pluginConfig.tmux?.agent_pane_min_width ?? 40,
} as const; } as const;
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName); const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
const safeHookEnabled = pluginConfig.experimental?.safe_hook_creation ?? true;
const modelCacheState = createModelCacheState(); const modelCacheState = createModelCacheState();
const contextWindowMonitor = isHookEnabled("context-window-monitor") const contextWindowMonitor = isHookEnabled("context-window-monitor")
? createContextWindowMonitorHook(ctx) ? safeCreateHook("context-window-monitor", () => createContextWindowMonitorHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const preemptiveCompaction = const preemptiveCompaction =
isHookEnabled("preemptive-compaction") && isHookEnabled("preemptive-compaction") &&
pluginConfig.experimental?.preemptive_compaction pluginConfig.experimental?.preemptive_compaction
? createPreemptiveCompactionHook(ctx) ? safeCreateHook("preemptive-compaction", () => createPreemptiveCompactionHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const sessionRecovery = isHookEnabled("session-recovery") const sessionRecovery = isHookEnabled("session-recovery")
? createSessionRecoveryHook(ctx, { ? safeCreateHook("session-recovery", () => createSessionRecoveryHook(ctx, {
experimental: pluginConfig.experimental, experimental: pluginConfig.experimental,
}) }), { enabled: safeHookEnabled })
: null; : null;
// Check for conflicting notification plugins before creating session-notification // Check for conflicting notification plugins before creating session-notification
@ -166,17 +168,17 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
allPlugins: externalNotifier.allPlugins, allPlugins: externalNotifier.allPlugins,
}); });
} else { } else {
sessionNotification = createSessionNotification(ctx); sessionNotification = safeCreateHook("session-notification", () => createSessionNotification(ctx), { enabled: safeHookEnabled });
} }
} }
const commentChecker = isHookEnabled("comment-checker") const commentChecker = isHookEnabled("comment-checker")
? createCommentCheckerHooks(pluginConfig.comment_checker) ? safeCreateHook("comment-checker", () => createCommentCheckerHooks(pluginConfig.comment_checker), { enabled: safeHookEnabled })
: null; : null;
const toolOutputTruncator = isHookEnabled("tool-output-truncator") const toolOutputTruncator = isHookEnabled("tool-output-truncator")
? createToolOutputTruncatorHook(ctx, { ? safeCreateHook("tool-output-truncator", () => createToolOutputTruncatorHook(ctx, {
experimental: pluginConfig.experimental, experimental: pluginConfig.experimental,
}) }), { enabled: safeHookEnabled })
: null; : null;
// Check for native OpenCode AGENTS.md injection support before creating hook // Check for native OpenCode AGENTS.md injection support before creating hook
let directoryAgentsInjector = null; let directoryAgentsInjector = null;
@ -195,18 +197,18 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
}, },
); );
} else { } else {
directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx); directoryAgentsInjector = safeCreateHook("directory-agents-injector", () => createDirectoryAgentsInjectorHook(ctx), { enabled: safeHookEnabled });
} }
} }
const directoryReadmeInjector = isHookEnabled("directory-readme-injector") const directoryReadmeInjector = isHookEnabled("directory-readme-injector")
? createDirectoryReadmeInjectorHook(ctx) ? safeCreateHook("directory-readme-injector", () => createDirectoryReadmeInjectorHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const emptyTaskResponseDetector = isHookEnabled( const emptyTaskResponseDetector = isHookEnabled(
"empty-task-response-detector", "empty-task-response-detector",
) )
? createEmptyTaskResponseDetectorHook(ctx) ? safeCreateHook("empty-task-response-detector", () => createEmptyTaskResponseDetectorHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const thinkMode = isHookEnabled("think-mode") ? createThinkModeHook() : null; const thinkMode = isHookEnabled("think-mode") ? safeCreateHook("think-mode", () => createThinkModeHook(), { enabled: safeHookEnabled }) : null;
const claudeCodeHooks = createClaudeCodeHooksHook( const claudeCodeHooks = createClaudeCodeHooksHook(
ctx, ctx,
{ {
@ -219,84 +221,84 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const anthropicContextWindowLimitRecovery = isHookEnabled( const anthropicContextWindowLimitRecovery = isHookEnabled(
"anthropic-context-window-limit-recovery", "anthropic-context-window-limit-recovery",
) )
? createAnthropicContextWindowLimitRecoveryHook(ctx, { ? safeCreateHook("anthropic-context-window-limit-recovery", () => createAnthropicContextWindowLimitRecoveryHook(ctx, {
experimental: pluginConfig.experimental, experimental: pluginConfig.experimental,
}) }), { enabled: safeHookEnabled })
: null; : null;
const rulesInjector = isHookEnabled("rules-injector") const rulesInjector = isHookEnabled("rules-injector")
? createRulesInjectorHook(ctx) ? safeCreateHook("rules-injector", () => createRulesInjectorHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const autoUpdateChecker = isHookEnabled("auto-update-checker") const autoUpdateChecker = isHookEnabled("auto-update-checker")
? createAutoUpdateCheckerHook(ctx, { ? safeCreateHook("auto-update-checker", () => createAutoUpdateCheckerHook(ctx, {
showStartupToast: isHookEnabled("startup-toast"), showStartupToast: isHookEnabled("startup-toast"),
isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true, isSisyphusEnabled: pluginConfig.sisyphus_agent?.disabled !== true,
autoUpdate: pluginConfig.auto_update ?? true, autoUpdate: pluginConfig.auto_update ?? true,
}) }), { enabled: safeHookEnabled })
: null; : null;
const keywordDetector = isHookEnabled("keyword-detector") const keywordDetector = isHookEnabled("keyword-detector")
? createKeywordDetectorHook(ctx, contextCollector) ? safeCreateHook("keyword-detector", () => createKeywordDetectorHook(ctx, contextCollector), { enabled: safeHookEnabled })
: null; : null;
const contextInjectorMessagesTransform = const contextInjectorMessagesTransform =
createContextInjectorMessagesTransformHook(contextCollector); createContextInjectorMessagesTransformHook(contextCollector);
const agentUsageReminder = isHookEnabled("agent-usage-reminder") const agentUsageReminder = isHookEnabled("agent-usage-reminder")
? createAgentUsageReminderHook(ctx) ? safeCreateHook("agent-usage-reminder", () => createAgentUsageReminderHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const nonInteractiveEnv = isHookEnabled("non-interactive-env") const nonInteractiveEnv = isHookEnabled("non-interactive-env")
? createNonInteractiveEnvHook(ctx) ? safeCreateHook("non-interactive-env", () => createNonInteractiveEnvHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const interactiveBashSession = isHookEnabled("interactive-bash-session") const interactiveBashSession = isHookEnabled("interactive-bash-session")
? createInteractiveBashSessionHook(ctx) ? safeCreateHook("interactive-bash-session", () => createInteractiveBashSessionHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const thinkingBlockValidator = isHookEnabled("thinking-block-validator") const thinkingBlockValidator = isHookEnabled("thinking-block-validator")
? createThinkingBlockValidatorHook() ? safeCreateHook("thinking-block-validator", () => createThinkingBlockValidatorHook(), { enabled: safeHookEnabled })
: null; : null;
let categorySkillReminder: ReturnType<typeof createCategorySkillReminderHook> | null = null; let categorySkillReminder: ReturnType<typeof createCategorySkillReminderHook> | null = null;
const ralphLoop = isHookEnabled("ralph-loop") const ralphLoop = isHookEnabled("ralph-loop")
? createRalphLoopHook(ctx, { ? safeCreateHook("ralph-loop", () => createRalphLoopHook(ctx, {
config: pluginConfig.ralph_loop, config: pluginConfig.ralph_loop,
checkSessionExists: async (sessionId) => sessionExists(sessionId), checkSessionExists: async (sessionId) => sessionExists(sessionId),
}) }), { enabled: safeHookEnabled })
: null; : null;
const editErrorRecovery = isHookEnabled("edit-error-recovery") const editErrorRecovery = isHookEnabled("edit-error-recovery")
? createEditErrorRecoveryHook(ctx) ? safeCreateHook("edit-error-recovery", () => createEditErrorRecoveryHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const delegateTaskRetry = isHookEnabled("delegate-task-retry") const delegateTaskRetry = isHookEnabled("delegate-task-retry")
? createDelegateTaskRetryHook(ctx) ? safeCreateHook("delegate-task-retry", () => createDelegateTaskRetryHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const startWork = isHookEnabled("start-work") const startWork = isHookEnabled("start-work")
? createStartWorkHook(ctx) ? safeCreateHook("start-work", () => createStartWorkHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const prometheusMdOnly = isHookEnabled("prometheus-md-only") const prometheusMdOnly = isHookEnabled("prometheus-md-only")
? createPrometheusMdOnlyHook(ctx) ? safeCreateHook("prometheus-md-only", () => createPrometheusMdOnlyHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const sisyphusJuniorNotepad = isHookEnabled("sisyphus-junior-notepad") const sisyphusJuniorNotepad = isHookEnabled("sisyphus-junior-notepad")
? createSisyphusJuniorNotepadHook(ctx) ? safeCreateHook("sisyphus-junior-notepad", () => createSisyphusJuniorNotepadHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler") const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler")
? createTasksTodowriteDisablerHook({ ? safeCreateHook("tasks-todowrite-disabler", () => createTasksTodowriteDisablerHook({
experimental: pluginConfig.experimental, experimental: pluginConfig.experimental,
}) }), { enabled: safeHookEnabled })
: null; : null;
const questionLabelTruncator = createQuestionLabelTruncatorHook(); const questionLabelTruncator = createQuestionLabelTruncatorHook();
const subagentQuestionBlocker = createSubagentQuestionBlockerHook(); const subagentQuestionBlocker = createSubagentQuestionBlockerHook();
const writeExistingFileGuard = isHookEnabled("write-existing-file-guard") const writeExistingFileGuard = isHookEnabled("write-existing-file-guard")
? createWriteExistingFileGuardHook(ctx) ? safeCreateHook("write-existing-file-guard", () => createWriteExistingFileGuardHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const taskResumeInfo = createTaskResumeInfoHook(); const taskResumeInfo = createTaskResumeInfoHook();
const anthropicEffort = isHookEnabled("anthropic-effort") const anthropicEffort = isHookEnabled("anthropic-effort")
? createAnthropicEffortHook() ? safeCreateHook("anthropic-effort", () => createAnthropicEffortHook(), { enabled: safeHookEnabled })
: null; : null;
const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig); const tmuxSessionManager = new TmuxSessionManager(ctx, tmuxConfig);
@ -333,28 +335,28 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
); );
const atlasHook = isHookEnabled("atlas") const atlasHook = isHookEnabled("atlas")
? createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }) ? safeCreateHook("atlas", () => createAtlasHook(ctx, { directory: ctx.directory, backgroundManager }), { enabled: safeHookEnabled })
: null; : null;
initTaskToastManager(ctx.client); initTaskToastManager(ctx.client);
const stopContinuationGuard = isHookEnabled("stop-continuation-guard") const stopContinuationGuard = isHookEnabled("stop-continuation-guard")
? createStopContinuationGuardHook(ctx) ? safeCreateHook("stop-continuation-guard", () => createStopContinuationGuardHook(ctx), { enabled: safeHookEnabled })
: null; : null;
const compactionContextInjector = isHookEnabled("compaction-context-injector") const compactionContextInjector = isHookEnabled("compaction-context-injector")
? createCompactionContextInjector() ? safeCreateHook("compaction-context-injector", () => createCompactionContextInjector(), { enabled: safeHookEnabled })
: null; : null;
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer") const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
? createTodoContinuationEnforcer(ctx, { ? safeCreateHook("todo-continuation-enforcer", () => createTodoContinuationEnforcer(ctx, {
backgroundManager, backgroundManager,
isContinuationStopped: stopContinuationGuard?.isStopped, isContinuationStopped: stopContinuationGuard?.isStopped,
}) }), { enabled: safeHookEnabled })
: null; : null;
const unstableAgentBabysitter = isHookEnabled("unstable-agent-babysitter") const unstableAgentBabysitter = isHookEnabled("unstable-agent-babysitter")
? createUnstableAgentBabysitterHook( ? safeCreateHook("unstable-agent-babysitter", () => createUnstableAgentBabysitterHook(
{ {
directory: ctx.directory, directory: ctx.directory,
client: { client: {
@ -382,7 +384,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
backgroundManager, backgroundManager,
config: pluginConfig.babysitting, config: pluginConfig.babysitting,
}, },
) ), { enabled: safeHookEnabled })
: null; : null;
if (sessionRecovery && todoContinuationEnforcer) { if (sessionRecovery && todoContinuationEnforcer) {
@ -393,7 +395,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
} }
const backgroundNotificationHook = isHookEnabled("background-notification") const backgroundNotificationHook = isHookEnabled("background-notification")
? createBackgroundNotificationHook(backgroundManager) ? safeCreateHook("background-notification", () => createBackgroundNotificationHook(backgroundManager), { enabled: safeHookEnabled })
: null; : null;
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client); const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
@ -490,7 +492,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
}); });
categorySkillReminder = isHookEnabled("category-skill-reminder") categorySkillReminder = isHookEnabled("category-skill-reminder")
? createCategorySkillReminderHook(ctx, availableSkills) ? safeCreateHook("category-skill-reminder", () => createCategorySkillReminderHook(ctx, availableSkills), { enabled: safeHookEnabled })
: null; : null;
const skillMcpManager = new SkillMcpManager(); const skillMcpManager = new SkillMcpManager();
@ -515,7 +517,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
}); });
const autoSlashCommand = isHookEnabled("auto-slash-command") const autoSlashCommand = isHookEnabled("auto-slash-command")
? createAutoSlashCommandHook({ skills: mergedSkills }) ? safeCreateHook("auto-slash-command", () => createAutoSlashCommandHook({ skills: mergedSkills }), { enabled: safeHookEnabled })
: null; : null;
const configHandler = createConfigHandler({ const configHandler = createConfigHandler({
@ -610,7 +612,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
await stopContinuationGuard?.["chat.message"]?.(input); await stopContinuationGuard?.["chat.message"]?.(input);
await keywordDetector?.["chat.message"]?.(input, output); await keywordDetector?.["chat.message"]?.(input, output);
await claudeCodeHooks["chat.message"]?.(input, output); await claudeCodeHooks?.["chat.message"]?.(input, output);
await autoSlashCommand?.["chat.message"]?.(input, output); await autoSlashCommand?.["chat.message"]?.(input, output);
await startWork?.["chat.message"]?.(input, output); await startWork?.["chat.message"]?.(input, output);
@ -700,7 +702,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
event: async (input) => { event: async (input) => {
await autoUpdateChecker?.event(input); await autoUpdateChecker?.event(input);
await claudeCodeHooks.event(input); await claudeCodeHooks?.event?.(input);
await backgroundNotificationHook?.event(input); await backgroundNotificationHook?.event(input);
await sessionNotification?.(input); await sessionNotification?.(input);
await todoContinuationEnforcer?.handler(input); await todoContinuationEnforcer?.handler(input);
@ -800,10 +802,10 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
}, },
"tool.execute.before": async (input, output) => { "tool.execute.before": async (input, output) => {
await subagentQuestionBlocker["tool.execute.before"]?.(input, output); await subagentQuestionBlocker?.["tool.execute.before"]?.(input, output);
await writeExistingFileGuard?.["tool.execute.before"]?.(input, output); await writeExistingFileGuard?.["tool.execute.before"]?.(input, output);
await questionLabelTruncator["tool.execute.before"]?.(input, output); await questionLabelTruncator?.["tool.execute.before"]?.(input, output);
await claudeCodeHooks["tool.execute.before"](input, output); await claudeCodeHooks?.["tool.execute.before"]?.(input, output);
await nonInteractiveEnv?.["tool.execute.before"](input, output); await nonInteractiveEnv?.["tool.execute.before"](input, output);
await commentChecker?.["tool.execute.before"]?.(input, output); await commentChecker?.["tool.execute.before"]?.(input, output);
await directoryAgentsInjector?.["tool.execute.before"]?.(input, output); await directoryAgentsInjector?.["tool.execute.before"]?.(input, output);
@ -909,7 +911,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
} }
} }
await claudeCodeHooks["tool.execute.after"](input, output); await claudeCodeHooks?.["tool.execute.after"]?.(input, output);
await toolOutputTruncator?.["tool.execute.after"](input, output); await toolOutputTruncator?.["tool.execute.after"](input, output);
await preemptiveCompaction?.["tool.execute.after"](input, output); await preemptiveCompaction?.["tool.execute.after"](input, output);
await contextWindowMonitor?.["tool.execute.after"](input, output); await contextWindowMonitor?.["tool.execute.after"](input, output);
@ -924,7 +926,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
await editErrorRecovery?.["tool.execute.after"](input, output); await editErrorRecovery?.["tool.execute.after"](input, output);
await delegateTaskRetry?.["tool.execute.after"](input, output); await delegateTaskRetry?.["tool.execute.after"](input, output);
await atlasHook?.["tool.execute.after"]?.(input, output); await atlasHook?.["tool.execute.after"]?.(input, output);
await taskResumeInfo["tool.execute.after"](input, output); await taskResumeInfo?.["tool.execute.after"]?.(input, output);
}, },
"experimental.session.compacting": async ( "experimental.session.compacting": async (