整理这个博客的 PR 列表,发现一件好笑的事:同一个”发一篇博客”的动作,Claude 在我这儿有三条完全不同的执行路径,互相还不知道对方存在。
把 git log 和 PR 列表摊开,三种形态一眼能分出来:
344e6ba auto update from watcher62e031b post: 搜"disregard"把 Google 搞炸了——从一个词看 prompt injection 的攻击面1b78e70 auto update from watcherbecfe12 post: 今日要闻 5/23:美伊停火再临崩溃边缘……72 DRAFT claude/elegant-hypatia-ZXQ6m post: daily roundup June 7 202671 DRAFT claude/elegant-hypatia-kna7d post: 今日要闻(2026年6月6日)...62 OPEN dependabot-... build(deps): bump patch-updatesauto update from watcher、post: ...、还有一堆挂在 claude/elegant-hypatia-* 分支上的 draft PR——这是三套机制各自留下的痕迹。它们都在干同一件事:把一个 Markdown 文件塞进 src/content/posts/。但走的门、留的证据、有没有人审,完全不一样。
博客本身的架构我在 这个博客是怎么搭起来的 里写过(Astro + Fuwari + Cloudflare Pages),这里不重复。这篇只说”内容怎么进仓库”这一层——结果发现这一层是三头马车。
路径一:MCP connector(claude.ai 聊天里)
claude.ai 的网页聊天可以挂自定义 connector,本质是一个远程 MCP(Model Context Protocol)server。我给它接了一个 blog connector,暴露了个 submit_post 工具。在聊天框里说一句”把这篇发了”,Claude 调 submit_post,文章就进仓库了。
物证现在还躺在仓库里,是当时的连通性测试帖:
---title: "Test Post"description: "A quick test post submitted via the Claude blog MCP connector."draft: true---This is a test post submitted via the blog MCP connector from Claude.If you can read this, the integration is working correctly.关键点是:这个 connector 只在 claude.ai 的聊天会话里存在。 MCP 是个开放协议,但 claude.ai 的 connector、Claude Code CLI 本地配的 MCP、云端 routine 里配的 MCP,是三套互相独立的配置,不会自动共享。你在网页聊天里能调的工具,命令行里的 Claude 根本看不见。所以这条路只有我手动在网页上聊天时才会被触发——频率极低,基本是个摆设。
(顺带:MCP 和 Skills 的定位区别我在 MCP vs Skills 里掰扯过,这里把 MCP 当”发布通道”用,其实是它比较边缘的一种用法。)
路径二:draft PR(云端 routine)
每日要闻那一摞,走的是另一条路。我用 /schedule 配了个 routine,每天定点跑一次,让 Claude 抓当天新闻、写成稿子、提交。
这里得说清 routine 到底是什么,因为它和命令行里的 Claude Code 不是一回事:
- 它跑在 Anthropic 托管的云端,不依赖我的机器开着;
- 按标准 cron 调度,最小间隔 1 小时;
- 每次从默认分支 fresh clone 一份干净仓库开始干活;
- 产物默认推到
claude/前缀的分支上,开 PR。
claude/elegant-hypatia-* 这种分支名就是它的签名。而且这些 PR 默认是 draft 状态——官方文档在示例里提到 routine “打开 draft PR”,但没把”一定是 draft”写成规范,我这边实测下来确实清一色 draft。
这条路是三条里唯一有审查关卡的:PR 一开,CI 就在上面跑 Astro Check、Astro Build、Biome 三个 check。理论上很健康——内容先过构建,再合进 main。
理论上。
路径三:本地直推(CLI + watcher)
第三条最野:我在命令行的 Claude Code 会话里让它写篇文章,它用 Write 工具把 .md 落到磁盘,然后……就没有然后了,文件自己就上线了。
因为我本地挂了个目录监听脚本,防抖一段时间后自动 commit + push。实现细节在 macOS 上用 Zsh 做防抖自动提交 里,核心就这么一句:
commit_and_push() { # ... if git commit -m "auto update from watcher" >/dev/null 2>&1; then git push origin HEAD >/dev/null 2>&1 fi}所以 auto update from watcher 这个 commit message,根本不是 Claude 写的,是 watcher 替它擦的屁股——Claude 只管把文件写到磁盘,watcher 看到目录变了就直接怼上 main。
还有一类 post: 今日要闻…… 这种带正经 message 的 commit,是命令行会话里直接 git commit + push 的(作者名在 Steven Li、StevenLi、lishuyu 之间反复横跳,因为是不同机器、不同 git config 干的)。Claude Code 在有权限、又没设防护时,直接 push 到 main 是完全做得到的——社区甚至专门写了 branch-guard 之类的 hook 来拦这个行为。我没拦。
这条路没有任何关卡:不过 CI,不开 PR,写完即上线。
为什么会变成三头马车
根因不复杂:Claude 现在有一大堆互相独立的入口——命令行 CLI、网页版、云端 routine、claude.ai 聊天的 connector、GitHub Actions……每个入口的权限模型、文件系统访问、MCP 配置都不一样,但没有一个统一的”发布”抽象层。
于是”发博客”这个意图,落到哪个入口上,就被翻译成那个入口手头最顺的动作:
| 入口 | 发布动作 | 有没有审查 | 留下的痕迹 |
|---|---|---|---|
| claude.ai 聊天 | 调 MCP submit_post | 无 | test-post 那种 |
| 云端 routine | 开 draft PR | 有(CI) | claude/* 分支 |
| 本地 CLI | 写文件 / 直接 push | 无 | auto update from watcher / post: |
官方对”为什么要有这么多入口”基本没解释——文档里有 routine vs /loop vs Desktop task 的对比表,但没有一篇讲”它们的产物为什么不统一、该怎么收口”。这部分我只能算观察+推测:多入口是产品快速长出来的结果,发布这一层还没人去统一。
真正咬人的地方
光是”不统一”还只是强迫症犯了。真正咬人的是——唯一有保障的那条路(draft PR + CI),反而是最被晾着的那条。
今天清仓时发现,routine 开的 draft PR 从 5 月 11 号一路堆到 6 月 7 号,24 个全挂着没合。每天勤勤恳恳写稿、开 PR、跑 CI,然后……没人点合并,就这么积了快一个月。与此同时,直推那两条野路子天天往 main 上怼,畅通无阻。
合并这 24 个的时候又栽了一跤。我想偷懒批量 gh pr merge --auto,结果这仓库根本没开分支保护、也禁用了原生 auto-merge,--auto 直接报错,我的兜底逻辑退化成了普通 merge——而普通 merge 在没有 required check 的仓库里,不管 CI 红绿都照合。
后果:4 个 PR 的 CI 是红的,照样进了 main。红的原因都是同一种——frontmatter 里的引号没转义:
description: "……NASA"安静超音速"验证机 X-59……"# ^ 这个 ASCII 双引号把 YAML 字符串提前截断了双引号字符串里嵌了未转义的 ",YAML 直接报 bad indentation of a mapping entry,Astro 构建挂掉,Cloudflare Pages 部署被卡。中文内容改成全角引号 ""、英文带撇号的标题改用双引号包,构建才重新变绿。
讽刺的是:draft PR 这条路本来是唯一能在上线前拦住这种错误的。CI 早就把红叉打在 PR 上了。是我自己绕过了它。
收了个尾,但没治本
针对 draft PR 堆积,我加了个 workflow:routine 的草稿 PR 一旦 CI 全绿且可合并,就自动 draft→ready 并 squash 合并。
# 只认 claude/ 前缀的草稿 PR,全绿才合,绝不碰人工 PRif [ "$total" -gt 0 ] && [ "$bad" -eq 0 ] && [ "$mergeable" = "MERGEABLE" ]; then gh pr ready "$num" --repo "$REPO" gh pr merge "$num" --repo "$REPO" --squash --delete-branchfi这下 routine 那条路总算能闭环了,而且是真·“绿了才合”,不会再像我手动那样把红的也放进去。
但这只是给三条路里的一条打了个补丁。MCP connector 那条还是个摆设,本地直推那条还是裸奔上 main。三套机制依然各发各的,谁也不知道谁。
真正该有的,是一个统一的发布入口——不管意图从哪个 Claude 入口进来,最后都收口到同一条”过 CI → 合并”的管线上。在那之前,git log 里这三种 commit 形态还会继续并存。
我之前吐槽过 claude.ai 的 Gmail connector 写好邮件只肯存草稿、不肯发,是同一个病的另一个症状:每个集成都只把自己那一小段做完,没人管整条链路顺不顺。发博客这事儿,我现在有三个”只做完一半”的方案。加起来,也还是不到一个。