`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`:三份文件的同步与回填保护
如果你手写过 CLAUDE.md,那么用任何一个工具去「管理提示词」都值得先问一句:它切换的时候,我原来那份手写内容去哪了。CC Switch 的 Prompts 功能不是往某个私有库里塞条目,它写的就是各个 CLI 真正会读的那份文件。所以这件事的风险不在功能本身,在切换的那一瞬间。
好在这段逻辑集中、可读,主体落在两个文件里:src-tauri/src/prompt_files.rs(74 行)与 src-tauri/src/services/prompt.rs(242 行);另有一条深链入口在 src-tauri/src/deeplink/prompt.rs,最终仍汇进同一段逻辑。下面每一条都标了位置,你可以自己去核。
以下基于我们本地 clone 的 cc-switch 仓库快照 c39c903(提交日期 2026-08-10),仓库内版本号 3.19.2。我们只读源码文本,没有安装也没有运行过这个桌面应用。
文件名只有一个决策点
prompt_file_path() 是唯一的文件名决策点(src-tauri/src/prompt_files.rs:12)。要确认某个应用最终写的是哪份文件,只看这一个函数就够了,不用去翻别的地方。
映射表在 src-tauri/src/prompt_files.rs:32-39:
| 应用 | 提示词文件名 |
|---|---|
| Claude | CLAUDE.md |
| Codex | AGENTS.md |
| Gemini | GEMINI.md |
| GrokBuild | AGENTS.md |
| OpenCode | AGENTS.md |
| OpenClaw | AGENTS.md |
| Hermes | SOUL.md |
AppType::ClaudeDesktop 不走这张表,而是直接返回一个本地化错误 app.prompts_unsupported(中文文案「当前应用暂不支持 Prompts」,src-tauri/src/prompt_files.rs:13-19)。
这张表有两处容易读错,值得单独点出来。
第一,标题里的「三份文件」其实是四种文件名、七个应用。 四个应用共用同一个文件名 AGENTS.md。文件名只是拼接的最后一段,前面的目录由各应用各自的目录函数决定;目录来源这一层我们只核了两条:Claude 取 get_claude_settings_path() 的父目录、Codex 取 get_codex_auth_path() 的父目录,两者都带 home 下 .claude / .codex 的兜底分支(src-tauri/src/prompt_files.rs:21-30、59-74)。其余几个应用的目录来源我们没有核实,因此也不下「一定不会互相覆盖」这种结论——想确认你在用的那几个应用,得自己去读对应的目录函数。
第二,用户手册这张表只有四行。 docs/user-manual/zh/3-extensions/3.2-prompts.md:79-84 列的是 Claude、Codex、Gemini、OpenCode 四项;代码里另有 GrokBuild→AGENTS.md、OpenClaw→AGENTS.md、Hermes→SOUL.md(src-tauri/src/prompt_files.rs:32-39)。两处不一致,以我们实读的仓库状态为准。哪一处「才算数」、为什么会这样,本文不做推断。
顺带说一句测试覆盖:prompt_files.rs 里只有一个单测 hermes_prompt_file_uses_soul_md(src-tauri/src/prompt_files.rs:48-56),断言的是 Hermes 那一行。其余映射关系没有对应的测试用例。这只是静态计数,我们没有跑过任何测试。
反直觉的那一处:回填的落点有两种
真正需要讲透的是 enable_prompt(src-tauri/src/services/prompt.rs:73-91)。它做的第一件事不是写,而是读:切换之前先把 live 文件读出来,如果内容非空,就把这段内容回填到数据库里。
关键在于回填到哪。代码分两支:
- 当前有已启用项 → live 文件的内容被写进那一条已启用记录的
content,同时更新它的updated_at(src-tauri/src/services/prompt.rs:73-91)。 - 当前没有已启用项 → 新建一条备份记录:id 为
backup-{timestamp},name 为「原始提示词 {日期时间}」,description 为「自动备份的原始提示词」,enabled: false(src-tauri/src/services/prompt.rs:92-117)。
同一个动作,两种落点,差别不小。第二支是纯粹的新增,你手写的那份 CLAUDE.md 会变成列表里多出来的一条禁用记录,原有条目一条都不动。第一支则是覆盖:你在应用里给那条已启用记录写过的正文,会被 live 文件的当前内容顶掉。如果你既在应用里维护过这条记录,又在编辑器里直接改过 CLAUDE.md,那么切换之后留在库里的是编辑器那一版,应用里原来那一版不再存在于这条记录上。
这就是这段逻辑反直觉的地方:它的名字叫「保护」,保护的对象是磁盘上的 live 文件——不让你手写的内容凭空消失;它并不保证数据库里那条记录的旧内容不被覆盖。方向是从文件流向数据库,不是反过来。
备份分支还有一个去重条件:只要库里已有任意一条提示词的 content.trim() 与 live 内容相同,就不再新建备份(src-tauri/src/services/prompt.rs:94-97)。所以你不会因为反复切换攒出一堆一模一样的 backup- 条目;反过来说,内容只要差一个字符(trim 只去首尾空白),就会被判成不同,产生新的备份条目。
回填做完之后才是切换本身:先把该应用下所有 prompt 的 enabled 置为 false,再把目标那条置为 true,并原子写入文件(src-tauri/src/services/prompt.rs:126-133)。启用是单选,同一个应用同一时刻只有一条生效。
全部禁用 = 把文件写成空字符串
upsert_prompt 那一头也有一个需要知道的行为。若传入的条目 enabled 为真,就把 content 写到目标文件(src-tauri/src/services/prompt.rs:39-42);若为假,则回查该应用下是否还有其它启用项,一个都没有且文件存在时,写入空字符串(src-tauri/src/services/prompt.rs:44-54)。
注意语义:是清空,不是删除。落到 Claude 上就是 ~/.claude/CLAUDE.md 变成一个 0 字节的文件。这一步之后再想找回原来的内容,靠的就只能是上一节那条 backup- 记录——前提是它当初被创建出来了。
还有一条约束经常撞上:已启用的提示词不允许删除,delete_prompt 会直接返回「无法删除已启用的提示词」(src-tauri/src/services/prompt.rs:63-67)。结合「启用是单选」,想删掉当前这条,路径只有先启用另一条、让它退成禁用态,再删。
两个导入入口,enabled 默认值是相反的
Prompts 有两条把 live 文件收进库里的入口,默认值恰好相反,混起来会很困惑:
- 手动导入
import_from_file:生成 idimported-{timestamp},导入项enabled: false(src-tauri/src/services/prompt.rs:157-169)。 - 首次启动自动导入
import_from_file_on_first_launch:该应用已有提示词就跳过(幂等),文件不存在或内容为空返回 0,导入项 id 为auto-imported-{timestamp}且enabled: true(src-tauri/src/services/prompt.rs:187-240)。
也就是说,自动导入进来的那条是直接生效的,手动导入进来的那条不是。这三种 id 前缀 auto-imported- / imported- / backup- 对应三条来源;深链导入是第四条,它的 id 形如 {sanitized_name}-{timestamp_ms}(src-tauri/src/deeplink/prompt.rs:47-79),不带上述任何一个前缀,看 id 的时候要单独算一类。
另外,深链导入这条路径最终也会汇进同一段逻辑:ccswitch:// 的 prompt 载荷先以 enabled: false 落库,再按链接里的 enabled 参数决定要不要调 enable_prompt(src-tauri/src/deeplink/prompt.rs:47-79)。这里提它,是因为一旦走了 enable_prompt,上一节那套回填与备份就会照常触发——外部导入并不绕开它。ccswitch:// 协议本身的解析规则与四类载荷,我们另有一篇专门讲,这里不展开。
怎么自己核一遍
想确认你这台机器上会发生什么,按顺序做这几件事就够了,全都是读操作:
- 确认文件名:打开
src-tauri/src/prompt_files.rs:32-39,对照你在用的应用,拿到那份文件名;顺便看:13-19,确认你的应用不是 ClaudeDesktop 那一支(那一支会直接报错)。 - 确认当前状态:看目标文件此刻是不是非空。非空就意味着下一次启用会触发回填。
- 判断回填会走哪一支:看该应用当前有没有已启用的提示词。有 → 走覆盖分支,被覆盖的是那条已启用记录;没有 → 走
backup-{timestamp}新建分支。 - 切换之后验证:回到列表里找
backup-或看那条已启用记录的正文有没有变成 live 文件的内容,两者必居其一。 - 什么情况说明不是这套逻辑:如果 live 文件在切换前就是空的或只有空白字符,
enable_prompt的回填整段不会执行(判定条件是读出来的 live 文件内容 trim 后非空,src-tauri/src/services/prompt.rs:73-91),此时你没有备份是正常的,别去找backup-条目;同理,如果你发现文件被写成了空串,那不是回填的锅,是上一节upsert_prompt的全禁用分支(:44-54)。这两条能帮你把问题分到正确的一段代码上。
要提醒的是:这些文件是你本机的真实 CLI 配置,~/.claude/CLAUDE.md 这一类路径下的内容会被这套逻辑读写与清空。在把一份重要的手写提示词交给任何工具托管之前,自己另存一份副本是通用做法——这是我们给的一般性建议,不是该项目文档里的内容,项目本身也没有承诺任何保留策略。
最后是本文的边界:以上全部来自源码文本。手册里描述的操作流程与代码行为不一致的地方,我们只列位置、不评价;services/prompt.rs 之外的前端部分我们没有读,所以这套逻辑在应用里怎么呈现,本文一个字都不写。
本文依据 CC Switch 官方仓库(github.com/farion1231/cc-switch)的 README、docs/ 下的用户手册与发布说明、
src/config/ 的预设定义与 src-tauri/src/ 的后端源码整理,核对日 2026-08-10,对应仓库快照 c39c903。
本文内容为仓库源码与文档口径,我们没有安装或运行过这个桌面应用,
因此不涉及界面外观、操作手感与切换速度的任何描述。
文中出现的阈值与默认值均为源码中的默认配置,不构成对实际运行结果的保证。
该项目仍在快速迭代,版本与默认值随时可能变动,请以仓库最新内容为准。
安全相关做法请结合自身环境评估,本文不构成安全方案建议。