Claude Code 插件与 Hooks 架构
Claude Code 插件与 Hooks 架构:Claude-Mem 的具体实现
学习目标
本章聚焦 Claude-Mem 如何作为 Claude Code 插件,通过 6 个生命周期 Hook 实现持久记忆。建议先阅读前置知识,下文直接聚焦具体落地方案。
前置知识
本章涉及 Hook 系统和插件系统的通用架构,建议先阅读:
项目实践
插件目录结构
plugin/├── hooks/│ ├── hooks.json # Hook 配置 DSL│ └── codex-hooks.json # Codex 平台专用 Hook 配置├── scripts/│ ├── bun-runner.js # Bun 启动器,桥接 Node.js Hook 到 Bun Worker│ ├── worker-service.cjs # Worker Service 入口(CJS 格式)│ └── version-check.js # 版本检查脚本(Setup Hook,独立于统一 Worker)├── skills/ # 17 个 Skill 定义│ ├── mem-search/ # 记忆搜索 Skill│ ├── make-plan/ # 计划编排 Skill│ ├── do/ # 执行 Skill│ └── ...├── ui/│ └── viewer.html # Web Viewer UI(React 构建产物)└── modes/ # 工作模式定义 ├── code/ # 英文模式 ├── code--zh/ # 中文模式 └── code--ja/ # 日文模式6 个生命周期 Hook
统一 Worker 模式
除 version-check.js(Setup Hook)外,所有 Hook 通过统一路径执行:
node bun-runner.js worker-service.cjs hook <子命令>bun-runner.js 的作用:
- 检测 Bun 是否可用,不可用时自动安装
- 通过 Bun 执行
worker-service.cjs的hook子命令 - 将 Hook 参数(来自 Claude Code 的 stdin)转发给 Worker
这种模式的好处是:
- 代码复用:所有 Hook 共享同一个 Worker 实例的逻辑
- 热启动:Worker Service 常驻内存,Hook 只需 HTTP 调用
- 统一错误处理:所有 Hook 使用相同的退出码策略
Hook 退出码策略
Exit 0 — 成功或优雅关闭Exit 1 — 非阻塞错误(stderr 展示给用户,继续执行)Exit 2 — 阻塞错误(stderr 传给 Claude 处理)设计哲学:Worker/Hook 的错误默认 Exit 0,防止 Windows Terminal 标签页累积。包装层/插件层处理重启逻辑,ERROR 级别的日志保留用于诊断。
Hooks 配置 DSL
hooks.json 使用声明式语法定义每个 Hook 的行为:
| 字段 | 说明 |
|---|---|
matcher | 正则匹配,决定 Hook 何时触发(* 表示总是触发) |
type | Hook 类型(command 表示执行 shell 命令) |
shell | 使用的 shell(bash) |
command | 要执行的 shell 命令 |
timeout | 超时时间(毫秒) |
问题与规避
PATH 传播
问题:Hook 在 Claude Code 的受限环境中执行,可能找不到 Node.js、Bun 等工具。
规避:每个 Hook 命令都以 export PATH=... 开头,通过 $SHELL -lc 'echo $PATH' 获取登录 shell 的 PATH。在 Cygwin/MSYS 环境下还通过 cygpath -w 转换路径。
插件禁用检测
问题:用户可能在 Claude Code 设置中禁用了插件,但 Hook 仍然被触发。
规避:src/shared/plugin-state.ts 提供 isPluginDisabledInClaudeSettings() 函数,Worker Service 启动时检查该状态,禁用时优雅退出。
设计取舍
为什么 version-check.js 独立于统一 Worker?
Setup Hook 的 version-check.js 是唯一的独立脚本,不调用统一 Worker:
- 原因:Setup Hook 在 Worker 可能尚未安装或版本不兼容时执行,需要独立的版本检查
- 优势:即使 Worker 损坏,版本检查仍能运行,可以提示用户修复
- 代价:需要维护额外的独立脚本