refactor(claude-code-hooks): extract handlers and session state

Split hook into per-concern modules:
- handlers/ directory for individual hook handlers
- session-hook-state.ts: session-level hook state management
This commit is contained in:
YeonGyu-Kim
2026-02-08 16:22:17 +09:00
parent 0f145b2e40
commit d3a3f0c3a6
7 changed files with 510 additions and 409 deletions
@@ -0,0 +1,92 @@
import type { PluginInput } from "@opencode-ai/plugin"
import { loadClaudeHooksConfig } from "../config"
import { loadPluginExtendedConfig } from "../config-loader"
import {
executePreToolUseHooks,
type PreToolUseContext,
} from "../pre-tool-use"
import { appendTranscriptEntry } from "../transcript"
import { cacheToolInput } from "../tool-input-cache"
import type { PluginConfig } from "../types"
import { isHookDisabled, log } from "../../../shared"
export function createToolExecuteBeforeHandler(ctx: PluginInput, config: PluginConfig) {
return async (
input: { tool: string; sessionID: string; callID: string },
output: { args: Record<string, unknown> },
): Promise<void> => {
if (input.tool.trim() === "todowrite" && typeof output.args.todos === "string") {
let parsed: unknown
try {
parsed = JSON.parse(output.args.todos)
} catch {
throw new Error(
`[todowrite ERROR] Failed to parse todos string as JSON. ` +
`Received: ${
output.args.todos.length > 100
? output.args.todos.slice(0, 100) + "..."
: output.args.todos
} ` +
`Expected: Valid JSON array. Pass todos as an array, not a string.`,
)
}
if (!Array.isArray(parsed)) {
throw new Error(
`[todowrite ERROR] Parsed JSON is not an array. ` +
`Received type: ${typeof parsed}. ` +
`Expected: Array of todo objects. Pass todos as [{id, content, status, priority}, ...].`,
)
}
output.args.todos = parsed
log("todowrite: parsed todos string to array", { sessionID: input.sessionID })
}
const claudeConfig = await loadClaudeHooksConfig()
const extendedConfig = await loadPluginExtendedConfig()
appendTranscriptEntry(input.sessionID, {
type: "tool_use",
timestamp: new Date().toISOString(),
tool_name: input.tool,
tool_input: output.args,
})
cacheToolInput(input.sessionID, input.tool, input.callID, output.args)
if (isHookDisabled(config, "PreToolUse")) {
return
}
const preCtx: PreToolUseContext = {
sessionId: input.sessionID,
toolName: input.tool,
toolInput: output.args,
cwd: ctx.directory,
toolUseId: input.callID,
}
const result = await executePreToolUseHooks(preCtx, claudeConfig, extendedConfig)
if (result.decision === "deny") {
ctx.client.tui
.showToast({
body: {
title: "PreToolUse Hook Executed",
message: `[BLOCKED] ${result.toolName ?? input.tool} ${
result.hookName ?? "hook"
}: ${result.elapsedMs ?? 0}ms\n${result.inputLines ?? ""}`,
variant: "error" as const,
duration: 4000,
},
})
.catch(() => {})
throw new Error(result.reason ?? "Hook blocked the operation")
}
if (result.modifiedInput) {
Object.assign(output.args, result.modifiedInput)
}
}
}