ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

claude-mem Windows 平台加固全解:Hook 超时、僵尸端口、PowerShell 引号与 CRLF 修复地图

claude-mem Windows 平台加固全解:Hook 超时、僵尸端口、PowerShell 引号与 CRLF 修复地图 claude-mem Windows 平台加固全解Hook 超时、僵尸端口、PowerShell 引号与 CRLF 修复地图【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem本文围绕 claude-mem 仓库中的 Windows 平台加固专项Phase-03-Windows-Platform-Hardening展开系统梳理 12 个 Windows 专属缺陷簇——Hook 无限挂起、TIME_WAIT 僵尸端口、PowerShell 引号转义失败、CRLF 破坏 shebang、进程清理失败——的根因、修复方案与当前仓库中的实际落地状态。读完本文你可以理解 claude-mem 在 Windows 上每一处平台条件代码路径的设计动机并掌握在 hook 超时、端口释放、守护进程派生与进程树清理四个维度上排查同类问题的方法。背景为什么 Windows 是 claude-mem 的第二大缺陷簇claude-mem 的核心架构是「短命 hook 进程 常驻 worker 守护进程」Claude Code 等宿主在每次会话事件时拉起一个 hook 进程hook 进程再通过 HTTP 调用本地 workerBun 运行时上的 Express 服务完成记忆注入、观察与摘要。这套机制在 Unix 上相当顺滑但在 Windows 上会撞上五类操作系统级差异Hook 挂起AbortSignal.timeout()在 Bun 上曾触发 libuv 断言崩溃Promise.race回退方案又会泄漏定时器导致 hook 进程无限挂起、终端标签页堆积僵尸端口worker 关闭 HTTP 服务后Windows 的 TCP 协议栈会因 TIME_WAIT 让端口继续被占用数秒重启时立刻撞上EADDRINUSEPowerShell 引号规则Windows 守护进程派生走 PowerShellStart-Process其引号、路径展开规则与 Unix shell 完全不同含空格或$的路径会直接派生失败CRLF 换行autocrlftrue的 Git 检出会把脚本变成\r\n行尾破坏 shebang 与 JSON 解析进程清理taskkill、Get-CimInstance在长命令行、系统进程、进程树已退出等边缘场景下行为特殊。专项文档 Phase-03-Windows-Platform-Hardening.md 将该问题群定位为「第二大 issue 簇」并明确其业务影响这些 bug 让 claude-mem 对相当一部分 Windows 用户「基本不可用」。修复策略统一为平台条件代码路径 正确的超时处理、转义与进程生命周期管理。任务地图七项任务与完成状态专项共列出 7 项任务其中前 2 项hook 超时挂起、僵尸端口时序在文档中已标记完成其余为规划中的加固项任务文档状态当前仓库对应实现修复 Windows hook 超时与挂起已完成hook-constants.ts、worker-utils.ts、bun-runner.js修复僵尸端口清理时序EADDRINUSE 重试已完成Server.ts、GracefulShutdown.ts、HealthMonitor.tsPowerShell 引号与转义修复未勾选ProcessManager.ts 中的buildWindowsDaemonStartCommandCRLF shebang 问题.gitattributes未勾选仓库根目录 .gitattributes 已存在ProcessManager.ts进程枚举边缘场景未勾选kill-process-tree.ts、process-registry.tsWindows 专项测试未勾选health-monitor.test.ts 等构建与产物验证未勾选npm run build-and-sync流程注该 playbook 是 2026-03-29 批次 issue triage 的阶段性规划文档复选框状态反映当时的执行进度后文逐项对照当前仓库源码的实际状态。Hook 超时与挂起修复从常量到全链路平台条件超时常量所有 hook 侧超时统一收口在 hook-constants.ts 中当前值如下常量值用途源码注释HEALTH_CHECK3000msworker 健康检查健康 worker 响应 100msAPI_REQUEST30000mshook 的 API 调用长于健康探测但低于宿主 hook 上限HOOK_READINESS_WAIT10000ms单 hook 等待「正在启动中」的 worker 完成 DB/搜索初始化POST_SPAWN_WAIT15000msspawn 后等待 daemon 起来Linux 1smacOSChroma 6-8sREADINESS_WAIT30000msspawn 后等待 DB 搜索初始化通常 5sPORT_IN_USE_WAIT3000ms端口被占但健康检查失败时的等待POWERSHELL_COMMAND10000msPowerShell 进程枚举通常 1s 完成WINDOWS_MULTIPLIER1.5Windows 平台放大系数getTimeout()按平台放大基准超时hook-constants.tsexport function getTimeout(baseTimeout: number): number { return process.platform win32 ? Math.round(baseTimeout * HOOK_TIMEOUTS.WINDOWS_MULTIPLIER) : baseTimeout; }需要特别指出的是仓库中实际存在两套 Windows 放大系数hook 侧HOOK_TIMEOUTS.WINDOWS_MULTIPLIER 1.5用于 hook 进程内的所有 HTTP 超时以及 ProcessManager.ts 中getPlatformTimeout()的WINDOWS_MULTIPLIER 2.0用于 worker 生命周期管理侧如waitForPortFree的超时。专项文档中「用现有getPlatformTimeout()的 2.0x 系数把waitForPortFree的 Windows 超时从 3 秒提到 6 秒」正是指后一套从当前源码看hook-constants.ts 中PORT_IN_USE_WAIT的基准值仍为 3000ms读者对照时需注意两套系数各自的作用域。超时如何落到每一次 fetchworker-utils.ts 是 hook 侧唯一的 HTTP 出口。它提供了三层防护带超时的 fetch 封装worker-utils.tsexport async function fetchWithTimeout(url: string, init: RequestInit {}, timeoutMs: number): PromiseResponse { try { // AbortSignal.timeout (Node 18) replaces the manual setTimeout/clearTimeout // race. On expiry it aborts with a TimeoutError DOMException. return await fetch(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }); } catch (err: unknown) { // Preserve the historical timeout-error message (...timed out...) that // callers match on (hook-command.ts, server-beta-client.ts) if (err instanceof DOMException err.name TimeoutError) { throw new Error(Request timed out after ${timeoutMs}ms); } throw err; } }从源码演进看这里经历过从「Promise.racesetTimeoutWindows 下规避AbortSignal.timeout的 Bun 崩溃」到「统一AbortSignal.timeout 归一化TimeoutError消息」的迁移注释说明保留Request timed out after Nms这一历史错误文案是因为hook-command.ts、server-beta-client.ts等调用方按该文案做字符串匹配。无论底层机制如何变化不变式是——hook 代码路径上的每一个fetch()都必须有超时且超时必须来自下文的分级常量杜绝裸fetch。可覆盖的分级超时健康检查、就绪等待、API 请求三档超时可分别用环境变量覆盖且带上下界校验worker-utils.ts、worker-utils.ts环境变量默认值合法范围CLAUDE_MEM_HEALTH_TIMEOUT_MSgetTimeout(3000)500 ~ 300000CLAUDE_MEM_HOOK_READINESS_TIMEOUT_MSgetTimeout(10000)0 ~ 300000CLAUDE_MEM_API_TIMEOUT_MSgetTimeout(30000)500 ~ 300000readSettingsBackedTimeout()还允许从 worker 的settings.json读取同名键优先级为环境变量 settings 文件 默认值任何非法值都会记录Invalid name, using default警告并回退默认而不是崩溃。worker 不可达的「大声失败」机制worker-utils.ts 的recordWorkerUnreachable()把连续失败计数原子写入hook-failures.json达到阈值默认 3 次可用CLAUDE_MEM_HOOK_FAIL_LOUD_THRESHOLD调整后通过 bypass 通道写 stderr 并以退出码 2 终止。退出码语义同样定义在 hook-constants.tsSUCCESS: 0、BLOCKING_ERROR: 2。bun-runnerstdin 缓冲超时与 Windows 标签页策略hook 的实际入口是 bun-runner.js——一个 Node 进程负责找到 Bun、读 stdin 载荷、再 spawn Bun 执行真正的 hook 脚本。它与 Windows 挂起问题直接相关的实现有三处stdin 缓冲超时bun-runner.jscollectStdin()在 5 秒后强制结束等待并 resolve防止父进程不发end事件时 hook 永久卡死在输入阶段setTimeout(() { process.stdin.removeAllListeners(); process.stdin.pause(); resolve(chunks.length 0 ? Buffer.concat(chunks) : null); }, 5000);注意这里 resolve 后没有对定时器做显式clearTimeout——进程即将退出事件循环随之销毁泄漏影响有限专项文档要求「成功路径上通过clearTimeout清理泄漏的setTimeout引用」属于该文件的加固方向。cmd.exe shim 的 8191 字符环境上限bun-runner.js只有.cmd/.bat垫片才需要经cmd.exeshell: true解析出的bun.exe必须直接 spawn。注释引用 issue #3196hook 通过 login-shell 预置 PATH 使环境翻倍超过 cmd.exe 单变量约 8191 字符上限时cmd 会静默看到空 PATH报bun is not recognized而此前where bun明明成功。退出码 0 策略bun-runner.js、bun-runner.jshook 执行失败时以process.exit(0)收尾避免 Windows Terminal 标签页堆积——持久化的失败信号是CAPTURE_BROKEN标记文件与runner-errors.log而不是退出码。唯一例外是 Bun 未安装child.on(error)中退出 1因为那是用户环境问题必须让用户看见 stderr。专项文档中「给 bun-runner 增加 30 秒进程级硬超时超时杀子进程并以 0 退出」在文档中标记为完成项从当前 bun-runner.js 源码结构看子进程挂死的兜底目前主要依靠 stdin 5s 超时与宿主侧 hook 超时读者可在当前检出中以grep验证该硬超时是否存在。僵尸端口TIME_WAIT、双 500ms 延迟与 EADDRINUSE 探测关闭时序Windows 特有的前后置延迟Windows 上close()之后端口并不会立刻释放。仓库在两处关闭路径都做了平台条件延迟Server.ts 的close()closeAllConnections()之后若为win32先等 500ms 再调用server.close()close 完成后再等 500msGracefulShutdown.ts 的closeHttpServer()同样的「500ms → close → 500ms」结构并在后延迟处记录Waited for Windows port cleanup。closeHttpServer()还修复了另一个关键错误路径issue #3380 注释server.close(cb)在句柄未处于 listening 状态时会报ERR_SERVER_NOT_RUNNING旧代码在 reject 会中断整个收尾级联session 排空、MCP 关闭、Chroma 停止、DB 关闭、supervisor 停止现在将其视为「已达目的态」并 resolve。专项文档规划的更激进参数——把两处延迟从 500ms500ms 提到 1500ms1000ms合计 2.5s理由是「Windows TCP 协议栈对 localhost 的 TIME_WAIT 最长需要 4 秒」并在其「Done」备注中声称已实施、且给Server.ts的listen()增加了「Windows 下 EADDRINUSE 时等待 2s、最多重试 3 次」的循环。当前仓库检出中两处延迟仍是 500ms500msServer.ts 的listen()也是失败即 reject注释仅强调 #3380 的「绑定失败的句柄不得残留在 graceful shutdown 中」未见 EADDRINUSE 重试循环。从源码结构看规划中的重试参数在当前主干尚未生效或已回退这正是阅读 playbook 与实际代码对照时的典型分歧点建议在移植或排查前先以git log核实。端口探测与释放等待HealthMonitor.ts 是端口状态判定的中心isPortInUse(port)HealthMonitor.ts在 Windows 上走两级探测先向http://host:port/api/health发 HTTP 快速路径——活的 claude-mem worker 应答即判定占用非 ok 或抛错ECONNREFUSED、超时则落回net.createServer真实 bind 探测因为「只有确定性的 bind 尝试才能分辨端口是否真的空闲」端口可能被非 HTTP 进程占用。Unix 上直接 bind 探测。waitForPortFree(port, timeoutMs)HealthMonitor.ts每 500ms 轮询一次isPortInUse直到超时返回 false。调用方 worker-service.ts 传入getPlatformTimeout(15000)即 Unix 15s、Windows 30s2.0x 系数。waitForHealth/waitForReadiness以 500ms 间隔轮询/api/health、/api/readiness同样受平台放大后的超时约束。测试侧health-monitor.test.ts 用setTimeout(() cb({ code: EADDRINUSE }), 0)mock 了「HTTP 探测抛错 → socket 探测命中 EADDRINUSE → 判定端口占用」的完整降级链worker-daemon-port-race.test.ts 则以源码断言expect(source).toContain(code EADDRINUSE)守护端口竞争检测不被误删。这两处验证了专项文档「测试须可在任意平台运行、不依赖真实 Windows」的约定。配套机制端口占用时的 hook 行为worker 侧启动时若端口被旧进程占用worker-service.ts 显式识别EADDRINUSE并保证竞态中不会覆盖胜出者的 PID 文件hook 侧 worker-utils.ts 的waitForWorkerPortClosed()在 SIGKILL 陈旧 worker 后以「连接被拒绝」作为端口已释放的信号轮询等待默认 5s。整套设计的不变式是任何一次 hook 事件内最多回收一次陈旧 worker且陈旧 worker 必须用不可捕获的 SIGKILL 树杀killProcessTree(pid, { signalMode: immediate })防止旧版本执行自己的 handoff 逻辑引发重启风暴——这些是 Windows 加固之外、但与其深度耦合的生命周期约束。PowerShell 引号与转义Windows 守护进程派生全解单引号转义与 Start-Process 参数拼接Windows 下 worker 守护进程的派生实现在 ProcessManager.ts 的spawnDaemon()win32 分支。核心是buildWindowsDaemonStartCommand()ProcessManager.tsexport function buildWindowsDaemonStartCommand(runtimePath: string, scriptPath: string): string { const psSingleQuote (value: string) value.replace(//g, ); // Windows PowerShell 5.1 joins -ArgumentList elements with spaces WITHOUT // quoting them ... a script path under a spaced %USERPROFILE% splits into // multiple argv entries and bun exits instantly with Module not found (#3195). return Start-Process -FilePath ${psSingleQuote(runtimePath)} -ArgumentList (${psSingleQuote(scriptPath)},--daemon) -WindowStyle Hidden; }这段代码浓缩了专项文档审计出的全部引号规则PowerShell 单引号串内单引号必须翻倍这是 PS 单引号串唯一的转义方式-ArgumentList元素在 PS 5.1 拼接子进程原生命令行时不加引号所以含空格的%USERPROFILE%下脚本路径会被拆成多个 argv、Bun 报 Module not foundissue #3195。解法是在单引号串内嵌字面双引号使路径保持单一参数-FilePath是单字符串参数、不经过该拼接故无需处理整段脚本以Buffer.from(psScript, utf16le).toString(base64)编码后经powershell -NoProfile -EncodedCommand执行ProcessManager.ts绕开外层 shell 对引号、$的二次解释-NoProfile避免用户 profile 干扰stdio: ignorewindowsHide: true隐藏控制台窗口。对照专项文档提出的通用规则路径含空格须在单引号串内双引号包裹已落实路径反斜杠在JSON 序列化时需要\\转义但 PowerShell 直接调用不需要该规则针对 CursorHooksInstaller 的 hook 命令生成即文档所指src/services/integrations/CursorHooksInstaller.ts中escapedBunPath.replace(/\\/g, \\\\)的 JSON 专用转义需验证其回读时不会二次转义路径中$如$HOME必须转义或包裹在单引号内以防变量展开PS 单引号串天然不展开$上述-EncodedCommand方案已规避。文档建议新增src/utils/下的escapeForPowerShell()统一工具——在当前src/目录中检索不到该函数实际实现以内联的psSingleQuote形式存在于buildWindowsDaemonStartCommand中可视为「尚未抽公共工具」。派生链的其他环节Unix 分支用setsid若存在detachedstdio: ignore实现守护化Windows 分支则完全交给 PowerShellStart-Process两条路径最终都写入统一的环境sanitizeEnv过滤ProcessManager.tsspawn.ts 的spawnHidden()对非 Windows 命令一律附加windowsHide: true是「Windows 上绝不弹出控制台窗口」的基线约定hook 派生 worker 时受 worker-spawn-gate.ts 的 spawn 锁约束acquireSpawnLock/releaseSpawnLock见 worker-utils.ts保证 hook、MCP server、CLI 三条启动路径同一时刻只有一个派生者。CRLF 与 shebang当前仓库的 .gitattributes 实况专项文档建议在仓库根创建.gitattributes为 shell 脚本与 JS 入口强制 LF 行尾。当前仓库已存在根目录 .gitattributes实际内容为* textauto eollf plugin/scripts/*.cjs eollf plugin/scripts/*.js eollf *.png binary *.jpg binary *.jpeg binary *.ico binary *.gif binary *.woff binary *.woff2 binary *.ttf binary *.eot binary *.otf binary与 playbook 建议稿的差异值得注意当前文件用全局* textauto eollf覆盖了*.sh、*.js、install/public/*.sh等建议条目即所有文本文件检出为 LF再显式声明plugin/scripts/*.cjs|*.js并把字体、图片列为 binary 防止行尾转换污染。文档提到的两个检查点仍然有效bun-runner.js 首行若带 shebang 必须是 LF 行尾否则脚本执行失败——它在plugin/scripts/下恰好被eollf规则覆盖hooks.json 等 JSON 文件若混入\r会出现在字符串值内部造成解析差异——全局eollf同样兜底。验证手段即文档「Run build and verify」任务所述提交.gitattributes后在 Windows 侧重新 clonefile命令或十六进制检查确认无\r。进程枚举与清理的边缘场景Windows 侧的进程操作依赖两条 PowerShell/WMI 通道当前仓库中的使用点与专项文档列出的边缘场景可以逐条对照Get-CimInstance Win32_Process用于两处关键场景process-identity.ts按ProcessId过滤取CreationDate构造「启动时间令牌」start token用于 PID 文件所有权校验——防止 PID 复用后误杀无辜进程process-registry.ts 注释明确指出拿到复用 PID 去taskkill /PID n /T /F会连无辜进程整棵子树一起杀kill-process-tree.ts一次性枚举全量进程的ProcessId/ParentProcessId/StartToken生成 CSV重建进程树。 文档指出的边缘场景是超 8000 字符命令行会被 WQL 的CommandLineLIKE 截断、系统进程可能返回 Access denied——前者建议wmic process兜底后者需在 PowerShell 层 try-catch。taskkill /PID pid /T /Fkill-process-tree.ts 执行树杀时显式处理「进程已不存在」语义——注释说明 taskkill 对不存在的 PID 以退出码 128 退出且该退出码对「已退出」与部分失败两种情形都会发出需要结合 stderr 与后续存活探测区分taskkill报告进程已消失时仅记 debug 日志不抛错。这正是文档要求「检查退出码并抑制静默失败」的落地。跨平台存活探测文档建议新增isProcessAlive(pid)工具Unixprocess.kill(pid, 0)/ Windowstasklist /FI PID eq pid。当前检索src/未见该函数名等价的存活判定由 Server.ts/api/admin/doctor路由里的isPidAlive()来自 process-registry.ts承担用于向诊断接口报告每个注册进程是 alive 还是 dead。测试与验证策略专项文档对测试的要求可归纳为三条且已在现有测试资产中部分兑现约定优先先找现有*.test.ts的目录结构与命名约定。仓库测试按tests/src 对应目录/镜像组织例如tests/infrastructure/、tests/services/、tests/shared/Bun 环境下以*.test.ts命名。纯 mock、跨平台可运行health-monitor.test.ts 通过给net.Server的 error 事件注入{ code: EADDRINUSE }来验证端口占用判定不需要真实 Windows端口重试逻辑的同类测试应遵循同一模式mockEADDRINUSE→ 断言带延迟重试 → 断言端口释放后成功。构建产物验证npm run build-and-sync构建后 grep 产物中的 Windows 不安全模式PowerShell 串内未转义的$、缺失超时回退的 fetch并确认.gitattributes已提交。对于文档提出的escapeForPowerShell()单测空格、美元符、反斜杠、Unicode 四类路径在公共工具抽取之前等效覆盖对象就是 ProcessManager.ts 中的buildWindowsDaemonStartCommand——直接断言其输出对C:\Users\John Doe$\AppData\Local\claude-mem这类路径生成「单引号翻倍 内嵌双引号」的正确 PS 语句即可。小结Windows 加固的四条不变式对照专项文档与当前源码claude-mem 的 Windows 平台加固可以压缩为四条工程不变式hook 路径上的任何 I/O 都有界——fetch 必带分级超时可经CLAUDE_MEM_*_TIMEOUT_MS覆盖带上下界校验、stdin 读取 5s 上限、超时统一归一化错误消息端口生命周期容忍 TIME_WAIT——关闭路径在 Windows 上前后各留延迟、isPortInUse双级探测、waitForPortFree按 2.0x 系数放大超时并以「连接被拒绝」作为端口释放的判据PowerShell 命令按 PS 语义构造——单引号翻倍、-ArgumentList内嵌双引号防拆分、-EncodedCommand避免外层解释、cmd.exe 垫片才走shell: true且警惕 8191 字符环境上限失败信号不依赖退出码——hook 失败退出 0 防 Windows Terminal 标签页堆积持久信号是CAPTURE_BROKEN标记与错误日志进程树清理用启动时间令牌防 PID 复用误杀taskkill的「已消失」退出码被显式吸收。排查 Windows 用户问题时建议按此顺序定位先看 hook 是否卡死stdin/超时常量再看端口是否未释放waitForPortFree日志与 EADDRINUSE 计数然后检查派生命令Start-Process的引号与%USERPROFILE%空格路径最后核对检出行尾.gitattributes是否生效。这四层分别对应 hook-constants.ts、HealthMonitor.ts、ProcessManager.ts 与 .gitattributes均附相对路径可沿当前仓库继续深入。【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表