Summary (EN): importRuleBackupRecords unconditionally overwrites the user's real rule file (targetPath, e.g. ~/.claude/CLAUDE.md) during backup/snapshot import, without comparing the target's content hash and without going through the existing conflict-resolution flow — and it deletes that rule's local version history in the same loop. Both recovery paths are destroyed at once, silently. Confirmed present in v0.5.8 and in current main (0.6.0-beta.1).
Area
Sync / backup / restore
Platform
macOS Apple Silicon (arm64, macOS 26.4.1)
Version
0.5.8(桌面版,stable 通道)。代码引用同时对照 tag v0.5.8 与当前 main(0.6.0-beta.1),两者的覆盖行为一致。
What happened?
在一次包含 rules 的备份/快照导入之后,PromptHub 把托管副本里的旧内容写回了我本机的全局规则文件,覆盖掉我在 PromptHub 之外手写的内容,并且把该规则的本地版本历史整个删掉了。
观察到的具体表现:
claude-global 的 targetPath(~/.claude/CLAUDE.md)被替换成一份约三个月前的旧内容,我在那之后手写的修改全部消失。
- 六个内置全局规则(claude / codex / gemini / amp / windsurf / opencode)在同一秒内全部被写入,
_rule.json 的 updatedAt 均落在同一秒。
- 其中三条备份记录的
record.content 是空串,于是对应的三个目标文件被写成 0 字节。
data/rules/.versions/<ruleId>/ 目录被清空重建,原有历史快照消失,因此「历史快照预览 / 恢复到草稿」也无法找回内容。
因为覆盖是完全静默的(没有提示、没有冲突标记、日志里也没有记录),而规则文件平时不会主动去看,这类丢失很难被及时发现。我这边是在被反复覆盖多次、间隔两个多月之后才定位到原因。
What did you expect?
导入不应在未经确认的情况下覆盖与托管副本内容不一致的 target 文件。期望行为:
- 导入前对每条 record 比对 target 的内容哈希 ——
inspectRuleSyncState() 已经具备这个能力,只是这条路径没有调用它。
- 内容不一致时标记为
out-of-sync / conflict,交给已有的 resolveRuleConflict() 让用户显式选择保留哪一侧,而不是直接覆盖。
- 版本历史不应被无条件删除 —— 在 target 已被覆盖的情况下,它恰好是用户唯一的补救手段。
record.content === "" 的记录建议跳过或单独确认,避免把用户的目标文件清零。
CHANGELOG 0.5.5 已经为 Skill 更新引入了「本地修改冲突保护 / Local Edit Conflict Protection」(比较安装时哈希 / 当前本地哈希 / 最新远端哈希,冲突需用户显式选择覆盖)。rules 的导入路径目前缺少等价保护,建议与之对齐。
Steps to reproduce
- 让 PromptHub 接管任一全局规则(例如 Claude Code 的
~/.claude/CLAUDE.md),此时托管副本与目标文件一致。
- 在 PromptHub 之外直接编辑该目标文件(编辑器、脚本、其它工具皆可),使其与托管副本不一致。此时 App 内该规则应显示为
out-of-sync。
- 触发一次包含 rules 的备份/快照导入(Desktop 的备份恢复,或 CLI 的 workspace / sync restore)。
- 观察:目标文件被替换为 record 中的内容,第 2 步的本地修改丢失;
data/rules/.versions/<ruleId>/ 被清空重建。
相关代码 / Code references
packages/core/src/rules-workspace.ts @ tag v0.5.8,importRuleBackupRecords()(L1075 起),循环体内 L1098-L1102:
const meta = await resolveRuleMeta(record.id);
await writeManagedRule(meta, record.content);
const restoredSyncStatus = await writeTargetRule(meta, record.content); // L1100:无条件覆盖 target
await fsp.rm(getRuleVersionsDir(record.id), { recursive: true, force: true }); // 版本历史整体删除
writeTargetRule()(L469):
try {
await fsp.mkdir(path.dirname(meta.targetPath), { recursive: true });
await fsp.writeFile(meta.targetPath, content, "utf-8");
return "synced";
} catch {
return "sync-error";
}
几点补充:
- 这段既没有调用
inspectRuleSyncState(),也没有走 resolveRuleConflict() —— App 内已有的冲突保护机制在这条路径上被整体绕过。
fsp.writeFile 会跟随符号链接写穿。若 targetPath 是 symlink(用 dotfiles 管理工具托管 ~/.claude/CLAUDE.md 是常见做法),被改写的是链接指向的真实文件,用户更不容易察觉。
- 当前
main(0.6.0-beta.1)中对应位置为 rules-workspace.ts:1471,fsp.rm 已换成 replaceRuleVersions(),但无条件覆盖 target 的行为未变,writeTargetRule 的三个调用点中这一处仍然没有任何冲突检查。
触发链路(Desktop):
apps/desktop/src/renderer/services/database-backup.ts
→ window.api.rules.importRecords(records, { replace })
→ apps/desktop/src/main/ipc/rules.ipc.ts (IPC_CHANNELS.RULES_IMPORT_RECORDS)
→ importRuleBackupRecords(records, { replace })
CLI 侧:packages/core/src/cli/workspace-sync.ts 的 restoreCliWorkspaceSnapshot() 以 { replace: true } 调用同一函数。
Logs, screenshots, or extra context
- data/rules/global/<platform>/_rule.json:六条规则 updatedAt 落在同一秒
- data/rules/.versions/<ruleId>/:仅剩重建后的 0001.md,index.json mtime 与导入时刻一致
- data/.trash/<ISO>/:同一时刻新增多个条目(options.replace → removeMissingProjectRules)
- logs/startup.log:不记录这次导入,仅有 startup:* 与 recovery:candidates_detected 等启动阶段事件
排查时最大的困难是没有任何日志线索:目标文件被改写、版本历史被删除,都不产生日志。建议给 rules 导入加一条结构化日志(写了哪些 targetPath、各自 syncStatus、是否覆盖了 out-of-sync 的内容、跳过了哪些空内容 record),这对定位这类静默数据丢失会很有帮助。
Before submitting
Area
Sync / backup / restore
Platform
macOS Apple Silicon (arm64, macOS 26.4.1)
Version
0.5.8(桌面版,stable 通道)。代码引用同时对照 tagv0.5.8与当前main(0.6.0-beta.1),两者的覆盖行为一致。What happened?
在一次包含 rules 的备份/快照导入之后,PromptHub 把托管副本里的旧内容写回了我本机的全局规则文件,覆盖掉我在 PromptHub 之外手写的内容,并且把该规则的本地版本历史整个删掉了。
观察到的具体表现:
claude-global的targetPath(~/.claude/CLAUDE.md)被替换成一份约三个月前的旧内容,我在那之后手写的修改全部消失。_rule.json的updatedAt均落在同一秒。record.content是空串,于是对应的三个目标文件被写成 0 字节。data/rules/.versions/<ruleId>/目录被清空重建,原有历史快照消失,因此「历史快照预览 / 恢复到草稿」也无法找回内容。因为覆盖是完全静默的(没有提示、没有冲突标记、日志里也没有记录),而规则文件平时不会主动去看,这类丢失很难被及时发现。我这边是在被反复覆盖多次、间隔两个多月之后才定位到原因。
What did you expect?
导入不应在未经确认的情况下覆盖与托管副本内容不一致的 target 文件。期望行为:
inspectRuleSyncState()已经具备这个能力,只是这条路径没有调用它。out-of-sync/ conflict,交给已有的resolveRuleConflict()让用户显式选择保留哪一侧,而不是直接覆盖。record.content === ""的记录建议跳过或单独确认,避免把用户的目标文件清零。CHANGELOG
0.5.5已经为 Skill 更新引入了「本地修改冲突保护 / Local Edit Conflict Protection」(比较安装时哈希 / 当前本地哈希 / 最新远端哈希,冲突需用户显式选择覆盖)。rules 的导入路径目前缺少等价保护,建议与之对齐。Steps to reproduce
~/.claude/CLAUDE.md),此时托管副本与目标文件一致。out-of-sync。data/rules/.versions/<ruleId>/被清空重建。相关代码 / Code references
packages/core/src/rules-workspace.ts@ tagv0.5.8,importRuleBackupRecords()(L1075 起),循环体内 L1098-L1102:writeTargetRule()(L469):几点补充:
inspectRuleSyncState(),也没有走resolveRuleConflict()—— App 内已有的冲突保护机制在这条路径上被整体绕过。fsp.writeFile会跟随符号链接写穿。若targetPath是 symlink(用 dotfiles 管理工具托管~/.claude/CLAUDE.md是常见做法),被改写的是链接指向的真实文件,用户更不容易察觉。main(0.6.0-beta.1)中对应位置为rules-workspace.ts:1471,fsp.rm已换成replaceRuleVersions(),但无条件覆盖 target 的行为未变,writeTargetRule的三个调用点中这一处仍然没有任何冲突检查。触发链路(Desktop):
CLI 侧:
packages/core/src/cli/workspace-sync.ts的restoreCliWorkspaceSnapshot()以{ replace: true }调用同一函数。Logs, screenshots, or extra context
排查时最大的困难是没有任何日志线索:目标文件被改写、版本历史被删除,都不产生日志。建议给 rules 导入加一条结构化日志(写了哪些 targetPath、各自 syncStatus、是否覆盖了 out-of-sync 的内容、跳过了哪些空内容 record),这对定位这类静默数据丢失会很有帮助。
Before submitting
~/.claude/rules目录)与 [Bug]: 规则文件删除后重新扫描仍保留失效记录 #193(规则文件删除后仍保留失效记录)都是扫描/索引方向的问题,与本条的写入覆盖不同。0.5.8中仍然存在,且当前main未改变覆盖 target 的行为。