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": {
"type": "object",
"propertyNames": {

View File

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

View File

@@ -24,6 +24,22 @@ export const AgentNameSchema = z.enum([
"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({
model: z.string().optional(),
temperature: z.number().min(0).max(2).optional(),
@@ -62,6 +78,7 @@ export const OhMyOpenCodeConfigSchema = z.object({
$schema: z.string().optional(),
disabled_mcps: z.array(McpNameSchema).optional(),
disabled_agents: z.array(AgentNameSchema).optional(),
disabled_hooks: z.array(HookNameSchema).optional(),
agents: AgentOverridesSchema.optional(),
claude_code: ClaudeCodeConfigSchema.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 AgentOverrides = z.infer<typeof AgentOverridesSchema>
export type AgentName = z.infer<typeof AgentNameSchema>
export type HookName = z.infer<typeof HookNameSchema>
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 { BackgroundManager } from "./features/background-agent";
import { createBuiltinMcps } from "./mcp";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig } from "./config";
import { OhMyOpenCodeConfigSchema, type OhMyOpenCodeConfig, type HookName } from "./config";
import { log, deepMerge } from "./shared";
import * as fs from "fs";
import * as path from "path";
@@ -103,6 +103,12 @@ function mergeConfigs(
...(override.disabled_mcps ?? []),
]),
],
disabled_hooks: [
...new Set([
...(base.disabled_hooks ?? []),
...(override.disabled_hooks ?? []),
]),
],
claude_code: deepMerge(base.claude_code, override.claude_code),
};
}
@@ -135,6 +141,7 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
agents: config.agents,
disabled_agents: config.disabled_agents,
disabled_mcps: config.disabled_mcps,
disabled_hooks: config.disabled_hooks,
claude_code: config.claude_code,
});
return config;
@@ -142,34 +149,64 @@ function loadPluginConfig(directory: string): OhMyOpenCodeConfig {
const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const pluginConfig = loadPluginConfig(ctx.directory);
const disabledHooks = new Set(pluginConfig.disabled_hooks ?? []);
const isHookEnabled = (hookName: HookName) => !disabledHooks.has(hookName);
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
const sessionRecovery = createSessionRecoveryHook(ctx);
const todoContinuationEnforcer = isHookEnabled("todo-continuation-enforcer")
? createTodoContinuationEnforcer(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
// This prevents the continuation enforcer from injecting prompts during active recovery
if (sessionRecovery && todoContinuationEnforcer) {
sessionRecovery.setOnAbortCallback(todoContinuationEnforcer.markRecovering);
sessionRecovery.setOnRecoveryCompleteCallback(todoContinuationEnforcer.markRecoveryComplete);
}
const commentChecker = createCommentCheckerHooks();
const grepOutputTruncator = createGrepOutputTruncatorHook(ctx);
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
const directoryReadmeInjector = createDirectoryReadmeInjectorHook(ctx);
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
const thinkMode = createThinkModeHook();
const commentChecker = isHookEnabled("comment-checker")
? createCommentCheckerHooks()
: null;
const grepOutputTruncator = isHookEnabled("grep-output-truncator")
? createGrepOutputTruncatorHook(ctx)
: 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, {
disabledHooks: (pluginConfig.claude_code?.hooks ?? true) ? undefined : true,
});
const anthropicAutoCompact = createAnthropicAutoCompactHook(ctx);
const rulesInjector = createRulesInjectorHook(ctx);
const autoUpdateChecker = createAutoUpdateCheckerHook(ctx);
const anthropicAutoCompact = isHookEnabled("anthropic-auto-compact")
? createAnthropicAutoCompactHook(ctx)
: null;
const rulesInjector = isHookEnabled("rules-injector")
? createRulesInjectorHook(ctx)
: null;
const autoUpdateChecker = isHookEnabled("auto-update-checker")
? createAutoUpdateCheckerHook(ctx)
: null;
updateTerminalTitle({ sessionId: "main" });
const backgroundManager = new BackgroundManager(ctx);
const backgroundNotificationHook = createBackgroundNotificationHook(backgroundManager);
const backgroundNotificationHook = isHookEnabled("background-notification")
? createBackgroundNotificationHook(backgroundManager)
: null;
const backgroundTools = createBackgroundTools(backgroundManager, ctx.client);
const callOmoAgent = createCallOmoAgent(ctx, backgroundManager);
@@ -252,16 +289,16 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
},
event: async (input) => {
await autoUpdateChecker.event(input);
await autoUpdateChecker?.event(input);
await claudeCodeHooks.event(input);
await backgroundNotificationHook.event(input);
await todoContinuationEnforcer.handler(input);
await contextWindowMonitor.event(input);
await directoryAgentsInjector.event(input);
await directoryReadmeInjector.event(input);
await rulesInjector.event(input);
await thinkMode.event(input);
await anthropicAutoCompact.event(input);
await backgroundNotificationHook?.event(input);
await todoContinuationEnforcer?.handler(input);
await contextWindowMonitor?.event(input);
await directoryAgentsInjector?.event(input);
await directoryReadmeInjector?.event(input);
await rulesInjector?.event(input);
await thinkMode?.event(input);
await anthropicAutoCompact?.event(input);
const { event } = input;
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 error = props?.error;
if (sessionRecovery.isRecoverableError(error)) {
if (sessionRecovery?.isRecoverableError(error)) {
const messageInfo = {
id: props?.messageID as string | undefined,
role: "assistant" as const,
@@ -359,7 +396,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
"tool.execute.before": async (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()) {
updateTerminalTitle({
@@ -374,13 +411,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
"tool.execute.after": async (input, output) => {
await claudeCodeHooks["tool.execute.after"](input, output);
await grepOutputTruncator["tool.execute.after"](input, output);
await contextWindowMonitor["tool.execute.after"](input, output);
await commentChecker["tool.execute.after"](input, output);
await directoryAgentsInjector["tool.execute.after"](input, output);
await directoryReadmeInjector["tool.execute.after"](input, output);
await rulesInjector["tool.execute.after"](input, output);
await emptyTaskResponseDetector["tool.execute.after"](input, output);
await grepOutputTruncator?.["tool.execute.after"](input, output);
await contextWindowMonitor?.["tool.execute.after"](input, output);
await commentChecker?.["tool.execute.after"](input, output);
await directoryAgentsInjector?.["tool.execute.after"](input, output);
await directoryReadmeInjector?.["tool.execute.after"](input, output);
await rulesInjector?.["tool.execute.after"](input, output);
await emptyTaskResponseDetector?.["tool.execute.after"](input, output);
if (input.sessionID === getMainSessionID()) {
updateTerminalTitle({
@@ -402,4 +439,5 @@ export type {
AgentOverrideConfig,
AgentOverrides,
McpName,
HookName,
} from "./config";