配置只写一份(技能、MCP、记忆等),扇出到你所有的 AI 编程智能体。
位于 ~/.agents/ 的 Store(配置仓库)保存着你共享的每一份配置的唯一真实副本——技能、指令、MCP 服务器、斜杠命令、子智能体、钩子。agentstow sync 把它扇出到你安装的全部十个智能体:字节可以完全相同的地方用符号链接,不能相同的地方用渲染后的键级合并。没有状态文件,将来也不会有。
# Store — 你共享的一切,只有这一份真实副本 ~/.agents/ ├── skills/ research/ tdd/ code-review/ ├── commands/ ship.md triage.md ├── subagents/ reviewer.md ├── AGENTS.md 你的指令,只写一次 ├── mcp.json 标准 mcpServers 结构 └── hooks/ SessionStart.toml PreToolUse.toml
扇出
只改一个文件,每个智能体都能看到。
$ ls -l ~/.claude/skills/ research -> ../../.agents/skills/research tdd -> ../../.agents/skills/tdd code-review -> ../../.agents/skills/code-review $ ls -l ~/.codex/skills/ research -> ../../.agents/skills/research tdd -> ../../.agents/skills/tdd code-review -> ../../.agents/skills/code-review
加进 Store 的技能,会以相对符号链接的形式出现在 Claude Code、Codex、Cursor 和 pi 里——链接是计算出来的,不是手写的,所以整棵目录树被移动或在别处恢复之后依然有效。
opencode 和 oh-my-pi 本来就会自己读取 ~/.agents/,所以 agentstow 刻意不为它们写任何东西。我们自己的链接一旦悬空就会被清理;指向别处的链接属于外来项(Foreign),一律不动。
两种机制
能用符号链接就用符号链接,不能用就渲染。
技能、指令、斜杠命令和子智能体在每个智能体那里都可以字节完全相同,所以它们是同一个文件被十处引用——漂移在构造上就不可能发生。MCP 服务器和钩子存在于智能体自己也拥有的文件里,而且格式各不相同,所以采用渲染加键级合并:你的其他键原样保留,只有 Store 中点名的条目会被改写。
# ~/.agents/mcp.json — 只需写这一次
{
"mcpServers": {
"serena": {
"type": "stdio",
"command": "uvx",
"args": ["--from", "serena-agent", "serena"]
}
}
}
# ~/.codex/config.toml — 合并,而不是替换 model = "gpt-5.3-codex" # 原样保留 [mcp_servers.serena] command = "uvx" args = ["--from", "serena-agent", "serena"] [projects."/Users/you/work"] # 原样保留 trust_level = "trusted"
无状态
文件系统就是状态。
同类工具靠复制、渲染,并在你的配置旁边留一份记录来记住自己拥有什么。一旦记录与磁盘不一致,输掉的是你的手工修改。agentstow 完全不留记录,因为归属可以直接从磁盘上读出来。
- 链接身份
- 解析进 Store 的符号链接就是我们的:它会被规范化,悬空时会被清理。
- 名字身份
- 名字出现在 Store 里的 MCP 服务器,或命令出现在 Store 里的钩子,是我们的。同一文件里的其他名字都是外来项(Foreign),原样保留。
- 标记身份
- 完全由生成得到的文件带有一行注释声明这一点。有标记的是我们的;没有标记的出自别人之手。
克制
变体是特性,不是冲突。
$ agentstow status
claude (.claude/skills) 12 linked
variant plannotator — left alone
variant-identical old-notes — identical to the
Store, could be re-linked
foreign vendor-thing — not ours
mcp (mcp.json)
managed serena → claude
foreign xapi → claude — not in the
Store — left alone
2 items need attention — run `agentstow sync`.
遮蔽 Store 条目的真实目录是一个变体(Variant):有意为之,永远被保留,只有当内容与 Store 副本完全相同时才会被标出——去重因此是你主动的决定,而不是一次意外。
凡不是 agentstow 写下的都是外来项(Foreign)——只报告,绝不修改。退出码就是契约:0 干净,1 出错,2 有事可做。
覆盖范围
十个智能体,六大配置族。
| 智能体 | 技能 | 指令 | MCP | 命令 | 子智能体 | 钩子 |
|---|---|---|---|---|---|---|
| Claude Code | fan-out | import-line | key-merge | fan-out | fan-out | key-merge |
| Codex | fan-out | symlink | key-merge (TOML) | fan-out | none | key-merge |
| opencode | native | symlink | key-merge | fan-out | fan-out | none |
| pi | fan-out | symlink | none | none | none | none |
| oh-my-pi | native | symlink | native | native | none | none |
| Gemini CLI | none | symlink | key-merge | render | none | key-merge |
| Cursor | fan-out | none | key-merge | fan-out | none | none |
| Windsurf | none | symlink | key-merge | fan-out | none | none |
| Roo | none | rules-dir link | none | fan-out | none | none |
| Cline | none | none | key-merge | none | none | none |
安全
还没建立信任,也可以放心运行。
$ agentstow sync --dry-run
dry run — no changes will be made
claude (.claude/skills)
missing research
missing tdd
mcp (mcp.json)
missing serena → codex — not in this agent's config yet
3 changes would be made.
- 第二次 sync 是空操作
- 连续运行两次,第二次会报告 Everything is up to date,文件字节完全相同。
- 每次写入都是原子的
- 私有临时文件、fsync、再原子重命名——目标路径先经符号链接解析,所以 dotfiles 的接线不会被破坏。
- 密钥不进 Store,也不进终端
${env:VAR}在 sync 时解析,并从每一行输出中脱敏,Store 因此始终可以提交进版本库。- 它向 CI 汇报
status --json与mcp list --json,退出码2专门表示「有事要做」。
安装
macOS 与 Linux。
# 预编译二进制 — 无需工具链,没有 postinstall 脚本 npm install -g agentstow # 或从源码构建,需要 Rust 1.97+ cargo install agentstow # 然后,在一台你已经用了一段时间的机器上 agentstow init
init 先创建 Store,然后告诉你现有配置里有哪些可以接管——按配置族列出,并附上对应的命令。
边界
它不做什么。
不做跨机器同步。Store 是一个普通目录——用 git 或 chezmoi 做版本管理。自建同步就意味着自建冲突解决,而这场仗 git 早已赢了。
不做记忆同步。智能体记忆不是一种有明确定义的产物,agentstow 不会假装它是。
不支持 Windows,没有 GUI,没有守护进程,没有文件监听。它不负责安装技能——Store 里有什么就扇出什么,不管是谁放进去的。也没有 mcp add,因为添加服务器不过是编辑一个有文档的标准文件。