Skip to content

Commit d4a2323

Browse files
committed
feat(claude-code-hooks): add PreCompact hook support for experimental.session.compacting event
Implement PreCompact hook executor to inject additional context into session compaction prompts. Includes: - PreCompactInput/PreCompactOutput type definitions - Pre-compact executor implementation with context injection - Event handler registration for experimental.session.compacting - Config loading and disabled hooks support 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
1 parent bd8c43e commit d4a2323

File tree

5 files changed

+166
-0
lines changed

5 files changed

+166
-0
lines changed

src/hooks/claude-code-hooks/config-loader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface DisabledHooksConfig {
99
PreToolUse?: string[]
1010
PostToolUse?: string[]
1111
UserPromptSubmit?: string[]
12+
PreCompact?: string[]
1213
}
1314

1415
export interface PluginExtendedConfig {
@@ -47,6 +48,7 @@ function mergeDisabledHooks(
4748
PreToolUse: override.PreToolUse ?? base.PreToolUse,
4849
PostToolUse: override.PostToolUse ?? base.PostToolUse,
4950
UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit,
51+
PreCompact: override.PreCompact ?? base.PreCompact,
5052
}
5153
}
5254

src/hooks/claude-code-hooks/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface RawClaudeHooksConfig {
1414
PostToolUse?: RawHookMatcher[]
1515
UserPromptSubmit?: RawHookMatcher[]
1616
Stop?: RawHookMatcher[]
17+
PreCompact?: RawHookMatcher[]
1718
}
1819

1920
function normalizeHookMatcher(raw: RawHookMatcher): HookMatcher {
@@ -30,6 +31,7 @@ function normalizeHooksConfig(raw: RawClaudeHooksConfig): ClaudeHooksConfig {
3031
"PostToolUse",
3132
"UserPromptSubmit",
3233
"Stop",
34+
"PreCompact",
3335
]
3436

3537
for (const eventType of eventTypes) {
@@ -66,6 +68,7 @@ function mergeHooksConfig(
6668
"PostToolUse",
6769
"UserPromptSubmit",
6870
"Stop",
71+
"PreCompact",
6972
]
7073
for (const eventType of eventTypes) {
7174
if (override[eventType]) {

src/hooks/claude-code-hooks/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import {
1919
executeStopHooks,
2020
type StopContext,
2121
} from "./stop"
22+
import {
23+
executePreCompactHooks,
24+
type PreCompactContext,
25+
} from "./pre-compact"
2226
import { cacheToolInput, getToolInput } from "./tool-input-cache"
2327
import { recordToolUse, recordToolResult, getTranscriptPath, recordUserMessage } from "./transcript"
2428
import type { PluginConfig } from "./types"
@@ -31,6 +35,35 @@ const sessionInterruptState = new Map<string, { interrupted: boolean }>()
3135

3236
export function createClaudeCodeHooksHook(ctx: PluginInput, config: PluginConfig = {}) {
3337
return {
38+
"experimental.session.compacting": async (
39+
input: { sessionID: string },
40+
output: { context: string[] }
41+
): Promise<void> => {
42+
if (isHookDisabled(config, "PreCompact")) {
43+
return
44+
}
45+
46+
const claudeConfig = await loadClaudeHooksConfig()
47+
const extendedConfig = await loadPluginExtendedConfig()
48+
49+
const preCompactCtx: PreCompactContext = {
50+
sessionId: input.sessionID,
51+
cwd: ctx.directory,
52+
}
53+
54+
const result = await executePreCompactHooks(preCompactCtx, claudeConfig, extendedConfig)
55+
56+
if (result.context.length > 0) {
57+
log("PreCompact hooks injecting context", {
58+
sessionID: input.sessionID,
59+
contextCount: result.context.length,
60+
hookName: result.hookName,
61+
elapsedMs: result.elapsedMs,
62+
})
63+
output.context.push(...result.context)
64+
}
65+
},
66+
3467
"chat.message": async (
3568
input: {
3669
sessionID: string
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type {
2+
PreCompactInput,
3+
PreCompactOutput,
4+
ClaudeHooksConfig,
5+
} from "./types"
6+
import { findMatchingHooks, executeHookCommand, log } from "../../shared"
7+
import { DEFAULT_CONFIG } from "./plugin-config"
8+
import { isHookCommandDisabled, type PluginExtendedConfig } from "./config-loader"
9+
10+
export interface PreCompactContext {
11+
sessionId: string
12+
cwd: string
13+
}
14+
15+
export interface PreCompactResult {
16+
context: string[]
17+
elapsedMs?: number
18+
hookName?: string
19+
continue?: boolean
20+
stopReason?: string
21+
suppressOutput?: boolean
22+
systemMessage?: string
23+
}
24+
25+
export async function executePreCompactHooks(
26+
ctx: PreCompactContext,
27+
config: ClaudeHooksConfig | null,
28+
extendedConfig?: PluginExtendedConfig | null
29+
): Promise<PreCompactResult> {
30+
if (!config) {
31+
return { context: [] }
32+
}
33+
34+
const matchers = findMatchingHooks(config, "PreCompact", "*")
35+
if (matchers.length === 0) {
36+
return { context: [] }
37+
}
38+
39+
const stdinData: PreCompactInput = {
40+
session_id: ctx.sessionId,
41+
cwd: ctx.cwd,
42+
hook_event_name: "PreCompact",
43+
hook_source: "opencode-plugin",
44+
}
45+
46+
const startTime = Date.now()
47+
let firstHookName: string | undefined
48+
const collectedContext: string[] = []
49+
50+
for (const matcher of matchers) {
51+
for (const hook of matcher.hooks) {
52+
if (hook.type !== "command") continue
53+
54+
if (isHookCommandDisabled("PreCompact", hook.command, extendedConfig ?? null)) {
55+
log("PreCompact hook command skipped (disabled by config)", { command: hook.command })
56+
continue
57+
}
58+
59+
const hookName = hook.command.split("/").pop() || hook.command
60+
if (!firstHookName) firstHookName = hookName
61+
62+
const result = await executeHookCommand(
63+
hook.command,
64+
JSON.stringify(stdinData),
65+
ctx.cwd,
66+
{ forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath }
67+
)
68+
69+
if (result.exitCode === 2) {
70+
log("PreCompact hook blocked", { hookName, stderr: result.stderr })
71+
continue
72+
}
73+
74+
if (result.stdout) {
75+
try {
76+
const output = JSON.parse(result.stdout) as PreCompactOutput
77+
78+
if (output.hookSpecificOutput?.additionalContext) {
79+
collectedContext.push(...output.hookSpecificOutput.additionalContext)
80+
} else if (output.context) {
81+
collectedContext.push(...output.context)
82+
}
83+
84+
if (output.continue === false) {
85+
return {
86+
context: collectedContext,
87+
elapsedMs: Date.now() - startTime,
88+
hookName: firstHookName,
89+
continue: output.continue,
90+
stopReason: output.stopReason,
91+
suppressOutput: output.suppressOutput,
92+
systemMessage: output.systemMessage,
93+
}
94+
}
95+
} catch {
96+
if (result.stdout.trim()) {
97+
collectedContext.push(result.stdout.trim())
98+
}
99+
}
100+
}
101+
}
102+
}
103+
104+
return {
105+
context: collectedContext,
106+
elapsedMs: Date.now() - startTime,
107+
hookName: firstHookName,
108+
}
109+
}

src/hooks/claude-code-hooks/types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type ClaudeHookEvent =
88
| "PostToolUse"
99
| "UserPromptSubmit"
1010
| "Stop"
11+
| "PreCompact"
1112

1213
export interface HookMatcher {
1314
matcher: string
@@ -24,6 +25,7 @@ export interface ClaudeHooksConfig {
2425
PostToolUse?: HookMatcher[]
2526
UserPromptSubmit?: HookMatcher[]
2627
Stop?: HookMatcher[]
28+
PreCompact?: HookMatcher[]
2729
}
2830

2931
export interface PreToolUseInput {
@@ -82,6 +84,13 @@ export interface StopInput {
8284
hook_source?: HookSource
8385
}
8486

87+
export interface PreCompactInput {
88+
session_id: string
89+
cwd: string
90+
hook_event_name: "PreCompact"
91+
hook_source?: HookSource
92+
}
93+
8594
export type PermissionDecision = "allow" | "deny" | "ask"
8695

8796
/**
@@ -166,6 +175,16 @@ export interface StopOutput {
166175
inject_prompt?: string
167176
}
168177

178+
export interface PreCompactOutput extends HookCommonOutput {
179+
/** Additional context to inject into compaction prompt */
180+
context?: string[]
181+
hookSpecificOutput?: {
182+
hookEventName: "PreCompact"
183+
/** Additional context strings to inject */
184+
additionalContext?: string[]
185+
}
186+
}
187+
169188
export type ClaudeCodeContent =
170189
| { type: "text"; text: string }
171190
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }

0 commit comments

Comments
 (0)