Coucou的Hook+Socket中继机制剖析:如何做到永不阻塞Claude Code
【免费下载链接】coucouA tiny friend that lives in your notch (macOS) or at the top of your screen (Windows, Linux) and keeps an eye on your coding agents: Claude Code, Gemini CLI, Antigravity and more.项目地址: https://gitcode.com/gh_mirrors/co/coucou
Coucou 是一款常驻在 Mac 刘海(Windows 则在屏幕顶部)的开源小工具,它通过一套Hook + Socket 中继机制实时盯着你的 Claude Code 会话,还能在刘海里直接审批权限。这套机制最"较真"的一条设计原则就是——永不阻塞 Claude Code:无论 Coucou 是否启动、是否崩溃、界面是否卡死,Claude Code 都会像没装它一样照常运行。本文就来拆解它到底是怎么做到的。
一、前提:Claude Code 自带的 Hook 机制
要理解中继,得先知道 Claude Code 本身提供了Hooks(钩子)能力。你可以在配置文件~/.claude/settings.json里,为一系列生命周期事件注册一条"要执行的命令"。比如会话开始、提交提示词、使用工具前后、请求权限、会话结束等:
SessionStart/SessionEnd:会话开始与结束UserPromptSubmit:你提交了一段提示词PreToolUse/PostToolUse:工具调用前后PermissionRequest:请求执行敏感操作,需要人工批准Stop/StopFailure:一次任务完成或失败
每当这些事件触发,Claude Code 就会把一段 JSON 通过标准输入(stdin)喂给你注册的命令,并等待命令结束。Coucou 要做的事情,就是注册一条"中继命令",把这段 JSON 转发给自己的主程序。
⚠️ 关键点:Claude Code 的耐心是有限的——每个 hook 都有一个超时(多数事件 10 秒,
PermissionRequest是 120 秒)。超时的后果是"命令被放弃、Claude Code 继续走"。所以中继脚本必须又快又稳。
二、中继脚本:把 stdin 转成一条 Socket 消息
中继脚本是"夹在 Claude Code 和 Coucou 主程序之间"的轻量角色。它负责两件事:读取 stdin 的 JSON、通过本地 Socket 发出去。
macOS:nb-hook+nb-hook.py(Unix 域套接字)
在 macOS 上,Coucou 会往~/Library/Application Support/NotchBuddy/写两个脚本:
nb-hook:Shell 外壳,由 Claude Code 直接调用。它内部调用 Python 中继脚本,并且无论发生什么都exit 0——这是"永不阻塞"的第一道保险。nb-hook.py:Python 中继,把 JSON 发往本地Unix 域套接字nb.sock(App Store 沙盒版本则发往容器内的路径)。
Windows:coucou-hook.exe(命名管道)
Windows 上对应的是一个用 Rust 编译的原生小工具coucou-hook.exe。它读取 stdin、补充一点终端上下文(比如TERM_PROGRAM),再把 JSON 通过命名管道\\.\pipe\coucou-<sid>发给主程序。管道名里带了当前用户的 SID,避免同一台机器上不同账号"撞"到同一条管道。
三、服务端:HookServer 与命名管道服务器
中继发出去的消息,由 Coucou 主程序里的"服务器"接收,再把事件翻译成界面状态。
macOS:HookServer(Swift,原生 Darwin Socket)
HookServer.swift 在后台线程上监听nb.sock,每来一个连接就开一个独立线程处理,绝不占用主线程。它有几条"防守":
- 只接受与当前用户同一 UID的连接(
getpeereid校验); - 并发连接上限32 个;
- 单条消息上限1 MB,收包超时5 秒;
- 套接字权限
0600、目录权限0700,别人读不到。
Windows:pipe.rs(Rust + tokio)
pipe.rs 用 tokio 的命名管道实现同样的逻辑。除PermissionRequest外,其它事件都是"转发即断开":把事件emit给岛窗口、然后立刻disconnect。
四、关键设计:为什么永远不会阻塞 Claude Code
这是整篇文章的"题眼"。Coucou 用五层兜底确保 Claude Code 永远不会被它卡住。
1. 外壳脚本"永远 exit 0"
macOS 的nb-hook脚本最后一行就是exit 0。哪怕 Python 中继出错、超时、崩溃,外壳都会安静地以 0 退出——对 Claude Code 而言,这就等于"命令正常执行完了"。
2. 极短的"连接超时" + 发后即忘
对绝大多数事件(非权限请求),中继采用fire-and-forget(发后即忘):
- macOS 的 Python 中继只给自己0.3 秒去连接;
- Windows 的
coucou-hook给连接300 毫秒,整个"发完就走"的事件给了2 秒预算。
如果 Coucou 没开、Socket/管道不存在,中继在几百毫秒内就放弃、空着 stdout 退出。结果就是:Claude Code 完全感受不到它的存在,会话照常推进。
3. 主线程"预算制",管道卡死也甩得掉
Windows 的 main.rs 把"真正会阻塞的读写"全丢到一个工作线程里,主线程则拿着一个截止时间。一旦工作线程超时(普通事件 2 秒),主线程直接退出进程——进程一死,管道句柄也随之释放,绝无可能把 Claude Code 卡死。
4. 只有PermissionRequest会等,而且要先确认"卡片真的可见"
权限审批是唯一需要"等人点按钮"的事件,但它也做了精心的"两段式"等待(见 pipe.rs):
- ACK 段(约 800 毫秒):岛窗口必须先回报"审批卡片已经显示在屏幕上"。如果界面被暂停、被别的东西挡住、甚至 webview 没在监听,这一步就失败——代价只是几百毫秒,而不是两分钟;
- 决策段(最长 108 秒):只有卡片确实在人眼前,才开始等用户点允许 / 拒绝。
macOS 上则是 HookServer.swift 把这条连接的文件描述符挂起保留,最长 115 秒。
5. 硬超时兜底,最终把决定权交回终端
不管上面哪一步失败、超时、Coucou 彻底无响应,最终都收敛到同一件事:中继不往 stdout 写任何东西。而"Claude Code 收到 hook 命令却没输出"的默认行为,就是——回到终端里照常询问。也就是说,最坏情况等价于"根本没装 Coucou",绝不会变成一次错误或死锁。
五、顺带看几个稳健性细节
除了"不阻塞",这套机制在"不乱改用户配置"上也很有分寸(见 hooks.rs):
- 先备份再写:写
settings.json前先做一份带时间戳的备份; - 只合并、不清空:只增删 Coucou 自己的条目,别人的 hook 原样保留;
- 先看 diff 再落盘:把变更差异给用户看,且用文件指纹校验"你看到的"和"要写入的"是同一份,中间被人改过就中止;
- 原子写入:先写到临时文件再重命名覆盖,磁盘写一半也不会留下半个坏文件。
六、关键文件索引
想自己翻代码验证的话,重点看这几处:
- macOS 中继与套接字服务器:NotchBuddy/Sources/App/HookServer.swift
- Windows 中继可执行文件:windows/hook/src/main.rs
- Windows 管道服务器:windows/src-tauri/src/pipe.rs
- hook 安装与 settings.json 安全写入:windows/src-tauri/src/hooks.rs
- 项目行为与设计原则:CLAUDE.md、README.md
一句话总结:Coucou 把 Claude Code 的 Hook 命令当成"一次性的、短命的、可以随时放弃的"中继",主程序才是真正长驻的服务端。无论这一端断了、卡了还是没人看,兜底逻辑都保证 Claude Code 最终会"回到终端继续干活"——这正是它敢承诺永不阻塞的底气所在。
【免费下载链接】coucouA tiny friend that lives in your notch (macOS) or at the top of your screen (Windows, Linux) and keeps an eye on your coding agents: Claude Code, Gemini CLI, Antigravity and more.项目地址: https://gitcode.com/gh_mirrors/co/coucou
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考