pre_compact / post_compact hooks:压缩前后的自定义扩展
Claude Code 的 hooks 系统允许用户在关键生命周期事件中插入自定义脚本。对于上下文压缩这个重要事件,系统提供了两个专用的 hooks:PreCompact(压缩前)和 PostCompact(压缩后)。
压缩 Hooks 的数据结构
两个 hooks 的输入 schema 都在 coreSchemas.ts 中通过 Zod 定义:
PreCompact Hook 输入
// source/src/entrypoints/sdk/coreSchemas.ts
export const PreCompactHookInputSchema = lazySchema(() =>
BaseHookInputSchema().and(
z.object({
hook_event_name: z.literal('PreCompact'),
// 触发方式:'manual'(用户执行 /compact)或 'auto'(自动触发)
trigger: z.enum(['manual', 'auto']),
// 当前的自定义压缩指令(可能为 null)
custom_instructions: z.string().nullable(),
}),
),
)
PostCompact Hook 输入
export const PostCompactHookInputSchema = lazySchema(() =>
BaseHookInputSchema().and(
z.object({
hook_event_name: z.literal('PostCompact'),
// 触发方式
trigger: z.enum(['manual', 'auto']),
// 压缩生成的会话摘要文本(完整内容)
compact_summary: z.string().describe(
'The conversation summary produced by compaction'
),
}),
),
)
BaseHookInputSchema 包含通用字段:session_id、transcript_path、cwd 等基础上下文信息。
executePreCompactHooks:压缩前的执行流程
// source/src/utils/hooks.ts
export async function executePreCompactHooks(
compactData: {
trigger: 'manual' | 'auto'
customInstructions: string | null
},
signal?: AbortSignal,
timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
): Promise<{
newCustomInstructions?: string // 钩子可以注入额外的压缩指令
userDisplayMessage?: string // 向用户展示的状态消息
}> {
const hookInput: PreCompactHookInput = {
...createBaseHookInput(undefined),
hook_event_name: 'PreCompact',
trigger: compactData.trigger,
custom_instructions: compactData.customInstructions,
}
const results = await executeHooksOutsideREPL({
hookInput,
matchQuery: compactData.trigger, // 按 trigger 类型匹配钩子
signal,
timeoutMs,
})
// 提取所有成功钩子的非空输出,合并为新的自定义指令
const successfulOutputs = results
.filter(result => result.succeeded && result.output.trim().length > 0)
.map(result => result.output.trim())
return {
newCustomInstructions:
successfulOutputs.length > 0 ? successfulOutputs.join('\n\n') : undefined,
userDisplayMessage: /* ... 构建展示消息 */,
}
}
关键特性:PreCompact hook 的 stdout 输出会被当作额外的压缩指令,追加到 customInstructions 中。这意味着你的 pre_compact 脚本可以动态影响压缩行为。
压缩流程中的 Hook 调用时机
在 compactConversation() 中,两个 hooks 在特定时间点被调用:
// source/src/services/compact/compact.ts(简化流程)
export async function compactConversation(...) {
// ① pre_compact hooks 在任何压缩工作开始前执行
context.onCompactProgress?.({ type: 'hooks_start', hookType: 'pre_compact' })
const hookResult = await executePreCompactHooks(
{ trigger: isAutoCompact ? 'auto' : 'manual', customInstructions },
context.abortController.signal,
)
// 将 hook 返回的指令合并进去
customInstructions = mergeHookInstructions(
customInstructions,
hookResult.newCustomInstructions,
)
// ② 执行实际压缩(调用 Claude 生成摘要)
const summary = await streamCompactSummary({ ... })
// ③ 重建消息列表
// ...
// ④ session_start hooks
context.onCompactProgress?.({ type: 'hooks_start', hookType: 'session_start' })
await processSessionStartHooks(...)
// ⑤ post_compact hooks 在所有重建工作完成后执行
context.onCompactProgress?.({ type: 'hooks_start', hookType: 'post_compact' })
const postHookResult = await executePostCompactHooks(
{ trigger: isAutoCompact ? 'auto' : 'manual', compactSummary: summary },
context.abortController.signal,
)
// ...
}
通过 matchQuery 匹配 trigger 类型
hooks 的 matchQuery 参数使用 compactData.trigger('manual' 或 'auto'),这意味着你可以配置只在特定触发方式下运行的 hook:
{
"hooks": {
"PreCompact": [
{
"matcher": "manual",
"hooks": [
{
"type": "command",
"command": "echo 'Manual compact triggered - saving work state'"
}
]
}
]
}
}
如果不指定 matcher,则对 manual 和 auto 两种触发方式都执行。
hooks.json 配置示例
Claude Code 的 hooks 通过 ~/.claude/hooks.json(全局)或项目级 .claudehooks.json 配置。以下是压缩 hooks 的完整配置示例:
示例 1:pre_compact 保存 Todo 列表
{
"hooks": {
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/save_todos.py"
}
]
}
]
}
}
save_todos.py 脚本示例:
#!/usr/bin/env python3
"""
pre_compact hook: 将当前任务状态保存到文件
这样即使压缩后 Claude 也能通过 CLAUDE.md 或文件记得待办事项
"""
import json
import os
import sys
from datetime import datetime
# 从环境变量读取会话信息
session_id = os.environ.get('CLAUDE_SESSION_ID', 'unknown')
cwd = os.environ.get('CLAUDE_CWD', os.getcwd())
# 输出额外的压缩指令(会被追加到压缩 prompt 中)
instructions = """
IMPORTANT: Please include in your summary:
1. Any incomplete tasks or TODOs that were mentioned
2. The exact file paths of files being modified
3. Any pending changes that haven't been saved
"""
print(instructions)
输出说明:pre_compact 脚本输出的内容会作为附加指令传给压缩模型,让摘要更好地保留特定信息。
示例 2:post_compact 将摘要写入日志
{
"hooks": {
"PostCompact": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'echo \"$COMPACT_SUMMARY\" >> ~/.claude/compact_log.txt'"
}
]
}
]
}
}
PostCompact hook 接收环境变量 COMPACT_SUMMARY(来自 hook input 的 compact_summary 字段),可以将其写入日志、发送通知或做其他处理。
示例 3:根据触发方式区分处理
{
"hooks": {
"PreCompact": [
{
"matcher": "auto",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Auto-compacting context...'"
}
]
}
],
"PostCompact": [
{
"matcher": "manual",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/scripts/analyze_compact_summary.py"
}
]
}
]
}
}
executePostCompactHooks 的输出处理
与 pre_compact 不同,post_compact hook 的 stdout 不会影响压缩结果(压缩已经完成),只用于显示消息:
export async function executePostCompactHooks(
compactData: {
trigger: 'manual' | 'auto'
compactSummary: string // 完整的压缩摘要文本
},
// ...
): Promise<{
userDisplayMessage?: string // 只返回展示消息,不影响压缩
}> {
// ...
// 成功时展示:
// "PostCompact [your-script.py] completed successfully: <输出>"
// 失败时展示:
// "PostCompact [your-script.py] failed: <错误输出>"
}
实际用例汇总
以下是 pre_compact / post_compact hooks 的几个实用场景:
| 场景 | hook 类型 | 实现方式 |
|---|---|---|
| 保存 todo 到文件 | PreCompact | 输出提示摘要要保留的关键信息 |
| 发送桌面通知 | PreCompact / PostCompact | notify-send 或 osascript |
| 记录压缩历史 | PostCompact | 将 compact_summary 写入日志 |
| 向摘要追加特定信息 | PreCompact | 脚本输出额外的摘要要求 |
| 验证压缩质量 | PostCompact | 分析摘要是否包含关键关键词 |
| CI 环境中禁用某些压缩行为 | PreCompact | 检测 CI 环境,输出特殊指令 |
Hook 执行失败时的行为
无论 pre_compact 还是 post_compact hook 失败,都不会中断压缩流程:
// hooks 失败只影响展示消息,不抛出异常
if (result.failed) {
displayMessages.push(`PreCompact [${result.command}] failed: ${result.output.trim()}`)
}
// 压缩继续进行
这是 Claude Code hooks 的一贯设计:hooks 是"观察者"而非"控制器",失败不应该阻止主流程。如果你需要在特定条件下阻止压缩,目前只能通过 REPL 层面的控制(如 DISABLE_AUTO_COMPACT 环境变量)而不是 hooks。