最近把一个自用小工具开源了:claude-bark-notify——一个 Claude Code 的通知 hook,把「任务跑完了」「等你选方案」「等你授权」「出错中断了」这些事件,端到端加密推送到 iPhone 上的 Bark。这篇记录一下为什么做它、过程中踩了什么坑、最后做成了什么样。

一、为什么要做这个

用 Claude Code 干活有个很典型的节奏:丢给它一个任务,它吭哧吭哧跑几分钟甚至更久。这段时间人是不会盯着终端的——去倒水、看别的东西、甚至离开电脑。然后问题就来了:

  • 干完了,我不知道,白白空等;
  • 它中途弹了个选择题(用哪个方案?要不要进入计划模式?),卡在那等我,我也不知道;
  • 它要执行某个命令在等授权,同样卡住;
  • 更糟的是 API 报错中断了,回来一看什么都没干。

四种情况都是同一个本质:Claude 在等我,而我不在。解法也很自然——让它在这些时刻给我手机推一条通知。

推送渠道选了 Bark,原因有两个。一是它够轻,一个 URL 就能推;二是它支持端到端加密:内容用 AES-256-CBC 加密后才发出去,Bark 服务器和苹果的 APNs 通道全程只见密文,解密秘钥只在我手机的 Bark App 里。通知内容是「Claude 刚帮我改了什么代码」,等于工作内容的实时摘要,明文过第三方服务器心里总归不踏实——加密这一层不是炫技,是这个工具能安心用的前提。

二、它是怎么挂上 Claude Code 的

Claude Code 提供了 hooks 机制:在特定事件发生时执行你指定的命令,事件上下文以 JSON 从 stdin 喂进来。这个项目用了四个钩子,正好对应上面四种「在等我」的时刻:

Hook时机推送标题示例
Stop每次回复结束要点 · 项目名
PreToolUse弹出选择题 / 方案待确认❓… · 待你选择 / 📋… · 方案待确认
Notification权限弹窗 / 空闲等待🔐… · 待授权 / ⏳… · 空闲等待
StopFailureAPI / 网络错误中断⚠️… · 异常中断

入口脚本按参数分发:无参数走 Stop 完成通知,--ask / --notify / --error 各走各的分支。后三类用不同的铃声和中断级别,手机上一听就知道是「干完了」还是「在等你」。

这里有一条贯穿整个项目的铁律:hook 永远 exit 0,任何失败静默兜底。通知脚本是挂在别人工作流上的寄生程序,它自己出任何问题——网络炸了、配置错了、依赖缺了——都只能写进日志自己消化,绝不能把异常抛回去阻塞 Claude 干活。配合 async: true 后台执行,主流程完全无感。

三、过程中踩的坑

坑一:推送发不出去

我的 Bark 走的是自建服务器,某些网络环境下,普通的 TLS 握手会被中间设备干扰,curl 直发时灵时不灵。排查了一圈,最后有效的办法是用 curl_cffi 模拟 Chrome 的 TLS 指纹——握手长得和浏览器一模一样,到达率立刻就上去了。

但这玩意儿要装 Python 依赖,不能强迫所有环境都用。最终传输层做成了一条逐级回退链

1
2
3
4
Chrome TLS 指纹(可选,curl_cffi)
→ 代理直发(可选,配置了才走)
→ curl 普通直发(--noproxy '*')
→ Node fetch 兜底

每一级失败就降级到下一级,全部失败也只是记条日志。另外有个容易忽略的细节:脚本启动时主动清空 HTTPS_PROXY 等环境变量——开发机上残留的代理设置曾经让请求莫名其妙走了根本不通的代理,查了半天。现在只有配置文件里显式写的代理才会生效。

坑二:通知没有信息量

最早的完成通知,正文就是「截取最后一条回复的开头」。看上去合理,实际收到全是「我已经完成了你要求的修改,具体来说…」这种废话开头——截断不等于总结,关键信息全在后面。

解法是让另一个模型来读上下文做真正的概括。这里有几个选型决策:

  • 用 OpenAI 兼容端点而不是绑死某家。绝大多数模型服务(DeepSeek、Kimi、GLM、各种中转)都兼容 /chat/completions,Node 原生 fetch 一个请求就能调,零 npm 依赖,跟整个项目的单文件脚本风格一致。我自己用的是 DeepSeek——国内直连、便宜、快,给推送写摘要绰绰有余。
  • 格式收得很死:标题 = 要点 · 项目名,正文首行是一句 ≤29 字的总结(锁屏上一眼能读完的长度),全文 ≤300 字。字数按 Unicode 码点算,emoji 算 1 个字,不然截断会把 emoji 劈成乱码。
  • 取材要干净。从 transcript 逆序扫描,找「最后一条 AI 回复」和「之前最近的一条真实用户提问」——中间要跳过 tool_result 和子代理的消息,否则喂给模型的全是工具调用的噪音。
  • 失败必须无感。LLM 调用设超时,失败或超时自动回退到原来的截断逻辑。总结是锦上添花,推送本身才是底线。

坑三:自用的「能跑」和开源的「能看」是两回事

决定开源时回头一看,代码自己都嫌弃:500 多行堆在一个文件里,秘钥写死在代码里,TLS 指纹是写死的主通道(等于强迫用户装 Python),AI 总结和主流程耦合,注释里还留着大段开发期的排障笔记。于是做了一轮重构:

  • 入口只留分发,逻辑拆进 lib/config / util / messages / summarize / transport 五个模块;
  • 秘钥全部外置到被 .gitignoreconfig.local.json,启动时深合并覆盖代码里的占位符,仓库和历史提交里不留任何敏感信息;
  • 一切增强皆可选:TLS 指纹默认关、AI 总结默认关、四个钩子按需安装,还有一层 events 配置开关可以不动 settings.json 临时静音某类通知;
  • 默认配置就是「开箱直发」,普通网络环境下零依赖可用。

重构后跑了消息构建的对拍测试,确认输出和重构前完全一致才合并。

四、最后做成了什么

一个零 npm 依赖的 Node 脚本(外加一个可选的 Python 发送器),完整能力:

  • 四类事件推送:完成 / 待选择 / 待授权 / 异常中断,各自独立可选;
  • 端到端加密:AES-256-CBC,服务器和推送通道只见密文;
  • 可选 LLM 总结:任何 OpenAI 兼容端点,失败自动回退;
  • 抗干扰传输链:TLS 指纹 → 代理 → 直发 → fetch 逐级回退;
  • 绝不添乱:永远 exit 0,失败只写日志,不阻塞 Claude。

仓库在这里:Ryonnoski0204/claude-bark-notify,MIT 协议。需要说明的是,这是按我自己的需求和网络环境打磨的自用工具,开源是「原样分享」——欢迎自取和 fork,但配置(尤其是服务器、秘钥、网络相关)请自行核对是否适合你。

回头看,这个小项目最大的收获不是代码本身,而是一种使用 AI 工具的姿态转变:与其守着终端等它,不如把「等」这件事工程化——它干完了叫我,要我拍板了叫我,出事了也叫我。人离开了电脑,活儿还在推进,这才是 agent 该有的样子。