feat(config): add disabled_hooks option for selective hook disabling

Allow users to individually disable built-in hooks via the
`disabled_hooks` configuration option in oh-my-opencode.json.

This addresses issue #28 where users requested the ability to
selectively disable hooks (e.g., comment-checker) that may
conflict with their workflow.

Available hooks:
- todo-continuation-enforcer
- context-window-monitor
- session-recovery
- comment-checker
- grep-output-truncator
- directory-agents-injector
- directory-readme-injector
- empty-task-response-detector
- think-mode
- anthropic-auto-compact
- rules-injector
- background-notification
- auto-update-checker

Closes #28
This commit is contained in:
Claude
2025-12-13 03:10:39 +00:00
committed by YeonGyu-Kim
parent 08e2bb4034
commit e131491db4
4 changed files with 114 additions and 34 deletions

View File

@@ -31,6 +31,28 @@
] ]
} }
}, },
"disabled_hooks": {
"type": "array",
"description": "List of built-in hooks to disable. Useful for selectively disabling hooks that may conflict with your workflow.",
"items": {
"type": "string",
"enum": [
"todo-continuation-enforcer",
"context-window-monitor",
"session-recovery",
"comment-checker",
"grep-output-truncator",
"directory-agents-injector",
"directory-readme-injector",
"empty-task-response-detector",
"think-mode",
"anthropic-auto-compact",
"rules-injector",
"background-notification",
"auto-update-checker"
]
}
},
"agents": { "agents": {
"type": "object", "type": "object",
"propertyNames": { "propertyNames": {

View File

@@ -4,6 +4,7 @@ export {
AgentOverridesSchema, AgentOverridesSchema,
McpNameSchema, McpNameSchema,
AgentNameSchema, AgentNameSchema,
HookNameSchema,
} from "./schema" } from "./schema"
export type { export type {
@@ -12,4 +13,5 @@ export type {
AgentOverrides, AgentOverrides,
McpName, McpName,
AgentName, AgentName,
HookName,
} from "./schema" } from "./schema"

View File

@@ -24,6 +24,22 @@ export const AgentNameSchema = z.enum([
"document-writer", "document-writer",
]) ])
export const HookNameSchema = z.enum([
"todo-continuation-enforcer",
"context-window-monitor",
"session-recovery",
"comment-checker",
"grep-output-truncator",
"directory-agents-injector",
"directory-readme-injector",
"empty-task-response-detector",
"think-mode",
"anthropic-auto-compact",
"rules-injector",
"background-notification",
"auto-update-checker",
])
export const AgentOverrideConfigSchema = z.object({ export const AgentOverrideConfigSchema = z.object({
model: z.string().optional(), model: z.string().optional(),
temperature: z.number().min(0).max(2).optional(), temperature: z.number().min(0).max(2).optional(),
@@ -62,6 +78,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
$schema: z.string().optional(), $schema: z.string().optional(),
disabled_mcps: z.array(McpNameSchema).optional(), disabled_mcps: z.array(McpNameSchema).optional(),
disabled_agents: z.array(AgentNameSchema).optional(), disabled_agents: z.array(AgentNameSchema).optional(),
disabled_hooks: z.array(HookNameSchema).optional(),
agents: AgentOverridesSchema.optional(), agents: AgentOverridesSchema.optional(),
claude_code: ClaudeCodeConfigSchema.optional(), claude_code: ClaudeCodeConfigSchema.optional(),
google_auth: z.boolean().optional(), google_auth: z.boolean().optional(),
@@ -71,5 +88,6 @@ export type OhMyOpenCodeConfig = z.infer<typeof OhMyOpenCodeConfigSchema>
export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema> export type AgentOverrideConfig = z.infer<typeof AgentOverrideConfigSchema>
export type AgentOverrides = z.infer<typeof AgentOverridesSchema> export type AgentOverrides = z.infer<typeof AgentOverridesSchema>
export type AgentName = z.infer<typeof AgentNameSchema> export type AgentName = z.infer<typeof AgentNameSchema>
export type HookName = z.infer<typeof HookNameSchema>
export { McpNameSchema, type McpName } from "../mcp/types" export { McpNameSchema, type McpName } from "../mcp/types"

View File

@@ -42,7 +42,7 @@ import { updateTerminalTitle } from "./features/terminal";
import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools"; import { builtinTools, createCallOmoAgent, createBackgroundTools } from "./tools";
import { BackgroundManager } from "./features/background-agent"; import { BackgroundManager } from "./features/background-agent";
import { createBuiltinMcps } from "./mcp"; import { createBuiltinMcps } from "./mcp";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config"; import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
import { log, deepMerge } from "./shared"; import { log, deepMerge } from "./shared";
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
@@ -103,6 +103,12 @@ function mergeConfigs(
...(override.disabled_mcps ?? []), ...(override.disabled_mcps ?? []),
]), ]),
], ],
disabled_hooks: [
...new Set([
...(base.disabled_hooks ?? []),
...(override.disabled_hooks ?? []),
]),
],
claude_code: deepMerge(base.claude_code, override.claude_code), claude_code: deepMerge(base.claude_code, override.claude_code),
}; };
} }
@@ -135,6 +141,7 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
agents: config.agents, agents: config.agents,
disabled_agents: config.disabled_agents, disabled_agents: config.disabled_agents,
disabled_mcps: config.disabled_mcps, disabled_mcps: config.disabled_mcps,
disabled_hooks: config.disabled_hooks,
claude_code: config.claude_code, claude_code: config.claude_code,
}); });
return config; return config;
@@ -142,34 +149,64 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
const OhMyOpenCodePlugin: Plugin = async (ctx) => { const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const pluginConfig = loadPluginConfig(ctx.directory); const pluginConfig = loadPluginConfig(ctx.directory);
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx); const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
const contextWindowMonitor = createContextWindowMonitorHook(ctx); ? createTodoContinuationEnforcer(ctx)
const sessionRecovery = createSessionRecoveryHook(ctx); : null;
const contextWindowMonitor = isHookEnabled("context-window-monitor")
? createContextWindowMonitorHook(ctx)
: null;
const sessionRecovery = isHookEnabled("session-recovery")
? createSessionRecoveryHook(ctx)
: null;
// Wire up recovery state tracking between session-recovery and todo-continuation-enforcer // Wire up recovery state tracking between session-recovery and todo-continuation-enforcer
// This prevents the continuation enforcer from injecting prompts during active recovery // This prevents the continuation enforcer from injecting prompts during active recovery
if (sessionRecovery && todoContinuationEnforcer) {
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering); sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete); sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
}
const commentChecker = createCommentCheckerHooks(); const commentChecker = isHookEnabled("comment-checker")
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx); ? createCommentCheckerHooks()
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx); : null;
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx); const grepOutputTruncator = isHookEnabled("grep-output-truncator")
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx); ? createGrepOutputTruncatorHook(ctx)
const thinkMode = createThinkModeHook(); : null;
const directoryAgentsInjector = isHookEnabled("directory-agents-injector")
? createDirectoryAgentsInjectorHook(ctx)
: null;
const directoryReadmeInjector = isHookEnabled("directory-readme-injector")
? createDirectoryReadmeInjectorHook(ctx)
: null;
const emptyTaskResponseDetector = isHookEnabled("empty-task-response-detector")
? createEmptyTaskResponseDetectorHook(ctx)
: null;
const thinkMode = isHookEnabled("think-mode")
? createThinkModeHook()
: null;
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, { const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true, disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
}); });
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx); const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact")
const rulesInjector = createRulesInjectorHook(ctx); ? createAnthropicAutoCompactHook(ctx)
const autoUpdateChecker = createAutoUpdateCheckerHook(ctx); : null;
const rulesInjector = isHookEnabled("rules-injector")
? createRulesInjectorHook(ctx)
: null;
const autoUpdateChecker = isHookEnabled("auto-update-checker")
? createAutoUpdateCheckerHook(ctx)
: null;
updateTerminalTitle({ sessionId: "main" }); updateTerminalTitle({ sessionId: "main" });
const backgroundManager = new BackgroundManager(ctx); const backgroundManager = new BackgroundManager(ctx);
const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager); const backgroundNotificationHook = isHookEnabled("background-notification")
? createBackgroundNotificationHook(backgroundManager)
: null;
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client); const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager); const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
@@ -252,16 +289,16 @@ 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 todoContinuationEnforcer.handler(input); await todoContinuationEnforcer?.handler(input);
await contextWindowMonitor.event(input); await contextWindowMonitor?.event(input);
await directoryAgentsInjector.event(input); await directoryAgentsInjector?.event(input);
await directoryReadmeInjector.event(input); await directoryReadmeInjector?.event(input);
await rulesInjector.event(input); await rulesInjector?.event(input);
await thinkMode.event(input); await thinkMode?.event(input);
await anthropicAutoCompact.event(input); await anthropicAutoCompact?.event(input);
const { event } = input; const { event } = input;
const props = event.properties as Record<string, unknown> | undefined; const props = event.properties as Record<string, unknown> | undefined;
@@ -313,7 +350,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const sessionID = props?.sessionID as string | undefined; const sessionID = props?.sessionID as string | undefined;
const error = props?.error; const error = props?.error;
if (sessionRecovery.isRecoverableError(error)) { if (sessionRecovery?.isRecoverableError(error)) {
const messageInfo = { const messageInfo = {
id: props?.messageID as string | undefined, id: props?.messageID as string | undefined,
role: "assistant" as const, role: "assistant" as const,
@@ -359,7 +396,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
"tool.execute.before": async (input, output) => { "tool.execute.before": async (input, output) => {
await claudeCodeHooks["tool.execute.before"](input, output); await claudeCodeHooks["tool.execute.before"](input, output);
await commentChecker["tool.execute.before"](input, output); await commentChecker?.["tool.execute.before"](input, output);
if (input.sessionID === getMainSessionID()) { if (input.sessionID === getMainSessionID()) {
updateTerminalTitle({ updateTerminalTitle({
@@ -374,13 +411,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
"tool.execute.after": async (input, output) => { "tool.execute.after": async (input, output) => {
await claudeCodeHooks["tool.execute.after"](input, output); await claudeCodeHooks["tool.execute.after"](input, output);
await grepOutputTruncator["tool.execute.after"](input, output); await grepOutputTruncator?.["tool.execute.after"](input, output);
await contextWindowMonitor["tool.execute.after"](input, output); await contextWindowMonitor?.["tool.execute.after"](input, output);
await commentChecker["tool.execute.after"](input, output); await commentChecker?.["tool.execute.after"](input, output);
await directoryAgentsInjector["tool.execute.after"](input, output); await directoryAgentsInjector?.["tool.execute.after"](input, output);
await directoryReadmeInjector["tool.execute.after"](input, output); await directoryReadmeInjector?.["tool.execute.after"](input, output);
await rulesInjector["tool.execute.after"](input, output); await rulesInjector?.["tool.execute.after"](input, output);
await emptyTaskResponseDetector["tool.execute.after"](input, output); await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
if (input.sessionID === getMainSessionID()) { if (input.sessionID === getMainSessionID()) {
updateTerminalTitle({ updateTerminalTitle({
@@ -402,4 +439,5 @@ export type {
AgentOverrideConfig, AgentOverrideConfig,
AgentOverrides, AgentOverrides,
McpName, McpName,
HookName,
} from "./config"; } from "./config";