首页
/ claude-mem 工单分诊 Phase 09:.git 防护、插件禁用状态与陈旧生成器修复实战

claude-mem 工单分诊 Phase 09:.git 防护、插件禁用状态与陈旧生成器修复实战

2026-09-05 16:53:43作者:滑思眉Philip

本篇以 claude-mem 仓库中 Issue 分诊系列(.maestro/playbooks/2026-02-23-Issue-Triage)的 Phase 09 修复文档为骨架,完整展开四个低优先级但影响面明确的修复:防止 CLAUDE.md 被写入 .git/ 导致仓库引用损坏(#1165)、尊重插件禁用状态(#781)、阻止 UserPromptSubmit 每轮重复注入上下文(#1079)、以及陈旧 AbortController 造成的队列停滞(#1099)。读完后,你将了解每个问题的根因定位方式、一行守卫与三层修复的具体实现,以及配套测试与回归验证的落点,可复用到任何"本地 Hook + 常驻 Worker"架构的 Agent 工具链中。

背景:Phase 09 在分诊体系中的定位

原始文档 TRIAGE-09-Minor-Fixes-And-Cleanup.md 将本阶段定义为"不影响核心功能的低优先级项"。文档开头给出了两个重要的前提判断,体现了务实的分诊策略:

  • 本阶段涉及的"安全"问题对 localhost-only 的应用而言大部分是理论风险,因此修复目标是防御性加固而非对抗真实攻击向量——例如 .git/ 防护被刻意设计为"每个写入点一行守卫",而不是引入一个 isInsideGitInternals() 工具模块,文档原话是"Focus on the actual write sites, not theoretical attack vectors";
  • 项目名冲突(project name collision)问题已在 Phase 03 解决,本阶段不再处理。

Phase 09 最终解决的 Issue 清单为:#1165(.git 损坏)、#781(插件禁用未被尊重)、#923(加载慢)、#1079(上下文重复注入)、#1099(陈旧 AbortController)。其中 #923 在文档中仅列入已解决清单、无独立任务展开;其余四项各有明确的修复任务、实现记录与测试计数。下文按文档的任务顺序逐一展开,并结合当前仓库源码核对实际落点。

修复一:阻止 CLAUDE.md 写入 .git/ 目录(#1165)

问题与修复策略

claude-mem 的核心机制之一是把压缩后的观察记录(observations)写成 CLAUDE.md(或 AGENTS.md)注入到项目目录,供后续会话自动加载。#1165 的问题是:某些路径解析异常时,这些文件可能被写进 .git/ 内部,而 .git/ 下的文件会被 Git 当作 refs/对象数据,直接损坏仓库引用。

文档给出的修复策略极简:搜索所有写入 CLAUDE.md/AGENTS.md 的代码路径,在每个写入点前加一个路径检查——

if (resolvedPath.includes('/.git/') || resolvedPath.includes('\\.git\\')) return;

文档特别强调这是"每写入点一行守卫",并点名了 4 个实际写入点。当前仓库源码与文档记录完全一致,且实际实现比文档中的最小示例更严格——同时覆盖了正斜杠、反斜杠两种分隔符,并追加了 endsWith 判断以拦截以 .git 结尾的路径:

// src/utils/claude-md-utils.ts(L80)
if (resolvedPath.includes('/.git/') || resolvedPath.includes('\\.git\\') || resolvedPath.endsWith('/.git') || resolvedPath.endsWith('\\.git')) return;

四个写入点及其守卫位置:

写入函数 文件 守卫行
writeClaudeMdToFolder() claude-md-utils.ts L80
writeAgentsMd() agents-md-utils.ts L10
writeClaudeMdToFolder() claude-md-commands.ts L242
writeClaudeMdToFolderForRegenerate() regenerate-claude-md.ts L243

claude-md-utils.ts 中的 writeClaudeMdToFolder() 为例,守卫位于 path.resolve() 之后、文件操作之前,即任何后续 writeFileSync/renameSync 都不可达 .git/ 路径。同一函数还体现了该写入通道的其他工程细节:通过 .tmp 临时文件 + renameSync 保证原子写入,并通过 toBmpSafe() 过滤 astral 平面字符(避免 Claude Code 截断代理对导致会话损坏)。文档记录该修复新增 4 个测试、提交 2616ac09;仓库中对应测试位于 claude-md-utils.test.ts

修复二:尊重插件禁用状态(#781)

问题与检查契约

用户在 Claude Code 中禁用 claude-mem 插件后(settings.json 中 enabledPlugins["claude-mem@thedotmack"] = false),插件的 Hook 入口和 Worker 服务应当完全静默。#781 的问题是禁用状态未被尊重,Hook 仍在触发。文档要求的修复契约:

  • 在每个 Hook 入口点顶部检查插件是否被禁用;
  • 读取 ~/.claude/settings.json 检查禁用插件列表;
  • 若被禁用,立即以退出码 0 结束——该检查必须是同步读取 + JSON 解析级别的快速操作,不能阻塞 Hook 链路。

核心实现:isPluginDisabledInClaudeSettings()

文档记录创建了 plugin-state.ts,当前源码与其描述一致,完整实现如下:

// src/shared/plugin-state.ts
const PLUGIN_SETTINGS_KEY = 'claude-mem@thedotmack';

export function isPluginDisabledInClaudeSettings(): boolean {
  try {
    const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
    const settingsPath = join(claudeConfigDir, 'settings.json');
    if (!existsSync(settingsPath)) return false;
    const raw = readFileSync(settingsPath, 'utf-8');
    const settings = parseJsonWithBom<Record<string, any>>(raw);
    return settings?.enabledPlugins?.[PLUGIN_SETTINGS_KEY] === false;
  } catch (error: unknown) {
    logger.error('CONFIG', 'Failed to read Claude settings', { ... });
    return false;
  }
}

从源码可以看出几个值得注意的设计点:

  1. 配置目录可覆盖:优先取 CLAUDE_CONFIG_DIR 环境变量,回退到 ~/.claude,便于多实例/测试场景隔离;
  2. 容错解析:通过 parseJsonWithBom(见 atomic-json.ts)处理带 BOM 的 JSON,避免 Windows 下手工编辑 settings.json 时解析失败;
  3. 失败即启用(fail-open):settings.json 不存在、JSON 解析异常或任何意外错误都返回 false(视为未禁用),保证禁用检查本身永远不会把正常用户挡在门外。

三个提前退出点

文档记录该检查被加入三个入口,当前仓库均可核对到:

  • worker-service.ts:main()——worker-service.ts 中,仅当命令为 undefined 或属于 Hook 触发类命令(start/hook/restart/daemon)时才执行禁用检查,被禁用则提前退出,避免 Hook 拉起整个 Worker;
  • bun-runner.js——bun-runner.js拉起 Bun 进程之前做同样的检查。该脚本内嵌了一份独立实现(不依赖 TS 模块),这是因为它运行在 Worker 启动链路的最前端,必须零依赖、零启动成本。测试 bun-runner.test.ts 会静态断言脚本源码中包含 isPluginDisabledInClaudeSettings() 函数定义;
  • smart-install.js——安装器在依赖检查之前同样先做禁用检查(按分诊文档记录),避免被禁用用户仍触发依赖安装。

文档记录该修复新增 7 个测试、全量 1070/1070 通过,提交 d6bc4495;仓库中的 plugin-disabled-check.test.ts 覆盖了 settings.json 不存在、enabledPlugins 缺失、键值为 true/非 false 等分支,均断言"未禁用",仅在显式为 false 时断言"已禁用"。

修复三:UserPromptSubmit 不再每轮重复注入上下文(#1079)

问题机理

claude-mem 的 UserPromptSubmit Hook 会在每次用户提交 prompt 时向 Worker 的 /api/sessions/init 发请求。#1079 的缺陷是:这个 Hook 在每一轮(而非仅首轮)都会触发完整的上下文注入与会话初始化,造成重复开销与上下文噪声。

文档要求的修复:通过 Worker API 引入会话级标志——一旦某会话已完成上下文注入,后续轮次跳过重复注入;注入前先查询 Worker 的会话状态。

实现:contextInjected 标志

当前源码与文档记录一致。Worker 侧在 /api/sessions/init 响应中计算并返回该标志,见 SessionRoutes.ts

const contextInjected = this.sessionManager.getSession(sessionDbId) !== undefined;

即:SessionManager 内存中已存在该会话的 SDK Agent 状态,说明注入已完成。Hook 侧的 session-init.ts 接收该响应字段:

logger.debug('HOOK', 'session-init: Received from /api/sessions/init',
  { sessionDbId, promptNumber, skipped: initResult.skipped, contextInjected: initResult.contextInjected });

行为边界被文档明确限定,值得强调:contextInjected=true 时跳过的是 POST /sessions/{sessionDbId}/init(SDK Agent 的重复初始化),而 prompt 追踪(/api/sessions/init 调用本身)仍然每轮执行——这样既消除了重复注入,又不破坏逐轮 prompt 记录。文档记录新增 6 个测试、1076/1076 通过。

修复四:陈旧 AbortController 队列停滞(#1099)

问题机理

Worker 为每个会话维护一个"生成器"(generator,负责驱动 SDK Agent 消费队列消息)。若生成器的 AbortController 处于陈旧/挂起状态,await 它的 Promise 会无限等待,整个会话队列停滞。#1099 正是这一症状。文档要求:搜索全部 AbortController 用法、为等待加 AbortSignal.timeout(30000)、超时后重置生成器状态。

三层修复

文档将其总结为"Three-layer fix",当前仓库源码可逐层对应:

第 1 层:活动心跳时间戳 lastGeneratorActivity 该字段定义在 worker-types.tsActiveSession 类型上,并在 SessionManager.ts 创建会话时初始化。三处刷新点与文档描述一一对应:

第 2 层:ensureGeneratorRunning() 中的陈旧检测。 SessionRoutes.tsensureGeneratorRunning() 是所有生成器启动的统一入口(init、summarize、overflow-recycle 等均经此)。修复逻辑为:若 lastGeneratorActivity 距今超过 30 秒阈值,说明现有 controller 已陈旧——中止它、重置生成器状态、启动一个全新的生成器,而不是继续在死状态上等待。

第 3 层:deleteSession() 的超时兜底。 SessionManager.ts 中删除会话时 await 卡死的生成器 Promise 不再无限挂起:

AbortSignal.timeout(30_000).addEventListener('abort', () => resolve(), { once: true });

即用一个 30 秒的超时信号给 Promise 加"逃逸出口"。测试侧对应 stale-abort-controller-guard.test.ts。文档记录该修复新增 10 个测试,958/958 通过(另有 24 个与本变更无关的既有失败)。

回归验证与遗留失败

Phase 09 的最后一项任务是"运行 npm test 并修复所有失败"。文档给出的结论:非既有测试全部通过;24 个既有失败集中在无关领域(需要真实服务的集成测试、ChromaSync、MarkdownFormatter 的 MCP 文本变化、openclaw、以及 transcripts/cli.ts 的日志规范检查),并明确"no regressions from #1099 fix"。这一阶段也示范了分诊文档的标准收尾方式:用失败清单的归因而非单纯计数来证明无回归。

小结:低优先级修复的工程标准

对照 Phase 09 的四项修复,可以提炼出该仓库处理"低优先级但影响明确"问题的通用方法:

  1. 最小侵入:#1165 用一行守卫 × 4 个实际写入点解决,拒绝抽象出防御理论攻击向量的工具模块;
  2. 快速检查放在链路最前端:#781 的禁用检查是同步读文件级别,且复制进 bun-runner.js 使其在 Bun 启动前就能生效;
  3. 状态驱动而非事件驱动的去重:#1079 用 Worker 内存会话状态(getSession() 是否存在)作为 contextInjected 的事实来源,Hook 侧保持无状态;
  4. 多层防御处理挂起:#1099 同时提供心跳检测、入口重置、AbortSignal.timeout 兜底,任何一层生效都能解除停滞。

相关文档与源码入口:TRIAGE-09 分诊文档plugin-state.tsclaude-md-utils.tsagents-md-utils.tsclaude-md-commands.tsregenerate-claude-md.tssession-init.tsSessionManager.tsSessionRoutes.ts,以及测试 plugin-disabled-check.test.tsbun-runner.test.tsclaude-md-utils.test.tsstale-abort-controller-guard.test.ts

登录后查看全文
热门项目推荐
相关项目推荐