Claude Code Windows 通知系统
概述
「Claude Code 只要停下来等你,就弹一条 Windows 系统通知」。
覆盖场景:请求权限 / 批准工具 / 提问 / 计划审批 / 空闲等待 / 任务完成 / API 报错 / 会话结束。
通知系统位于 C:\Users\YWH18\.claude\hooks\(2026-10-05 由 通知系统\ 改名而来)。职责单一——只发通知:不做审批决策、不修改权限模式、不拦截工具。
文件分工
| 文件 | 作用 |
|---|---|
notify_hook.py |
唯一入口:读 stdin 的 hook JSON → 生成中文文案 → 调用 toast.ps1 |
toast.ps1 |
WinRT Toast 弹窗,失败时自动退回托盘气泡 |
focus_terminal.py |
claude-notify:// 协议处理器,点击通知后把终端窗口拉回前台 |
settings.snippet.json |
通知系统的 5 个 hook 配置模板 |
notify.log |
运行日志(排错第一现场) |
notify_dedup.json |
去重状态,按窗口自动清理,无需手动维护 |
settings.snippet.json中不含PreToolUse。settings.json里那段block_builtin_web.py是内置 web 拦截,属于另一条链路,与本通知系统无关。
运行环境
使用独立安装版 Python 3.12.9(D:\APP\python312\python.exe)。该环境日常禁碰、禁止装包,不要换成 conda 的 D:/APP/anaconda3/envs/py312/。
五个 hook 事件(缺一有漏报)
| 事件 | 覆盖场景 |
|---|---|
PermissionRequest(matcher .*) |
请求权限 / 批准工具 / 提问 / 计划审批 |
Notification(排除 permission_prompt) |
空闲 60s、MCP 提问、子代理要输入、认证成功 |
Stop |
任务完成 |
StopFailure |
API 报错——Stop 在报错时不触发,只挂 Stop 会漏 |
SessionEnd |
会话异常退出 |
全部 "async": true,实测单次约 391ms,Claude 零等待。
当前配置(settings.json)
为什么 Notification 要排除 permission_prompt
permission_prompt 与 PermissionRequest 指向同一次权限请求,但前者晚约 6 秒触发、且每次击键都会重新计时。PermissionRequest 是即时信号,还自带工具名与内容,信息量严格更大。
notify_hook.py 的 main() 里还有一道显式丢弃(防回归):即使有人把 matcher 改回包含 permission_prompt,也不会双弹。
点击通知 → 跳回终端
完整链路:
协议注册(已就位,无需重做)
关键点:URL Protocol 必须是空字符串值。 缺它 Windows 不把这个协议识别为可激活协议,点击通知会被静默丢弃。
通知来源名(AUMID 注册)
通知来源显示的名字由注册表决定:
- 未注册 AUMID 也能正常弹通知,此时来源名直接显示 AUMID 字符串本身
- 注册只影响「显示什么名字」,不影响「能不能弹」
- 同名但在
...\Notifications\Settings\下的键是系统自动创建的每应用通知开关,不存 DisplayName,别改错位置 IconUri的真正约束是图片尺寸与体积:大图会导致图标渲染失败,换成小图即可(详见 Windows 系统设置合集 第七节)
去重机制
hook 每次触发都是全新进程,进程内状态下次必然为空,因此去重状态必须落盘到 notify_dedup.json。
去重指纹 = 事件 + session_id + cwd + 正文,四项齐全:
| 字段 | 防止的漏报/误杀 |
|---|---|
| 事件分类 | 区分 Stop / StopFailure / SessionEnd / 各类 Notification |
session_id |
同一会话 8 秒内第二次内容不同的批准被误杀 |
cwd |
两个项目输出相同正文时,第二个项目的「任务完成」被吞 |
| 正文 | 同一会话内连续两次内容不同的权限请求必须都通知 |
去重窗口 8 秒。读-改-写不加锁——最坏结果只是多弹一次,不会产生正确性问题。
排查
通知没弹
- 看
C:\Users\YWH18\.claude\hooks\notify.log - 确认
toast.ps1存在 - 手动发一条测试通知(不读 stdin):
注意:
Show()返回值不可信。 即使 AUMID 未注册、通知实际被丢弃,它同样返回成功,日志也照写「已通知」。判断通知是否真的弹出,只能看屏幕。
点击通知没反应
- 看
%TEMP%\claude-notify-focus.log——协议处理器被 Shell 拉起时没有 stdout/stderr/弹窗,这是唯一线索 - 确认
HKCU\Software\Classes\claude-notify下存在空的URL Protocol值 - 确认
toast.ps1显式设置了activationType="protocol"与launch
批量自测会丢通知
并发推送会静默丢失,且 Show() 与日志均无异常。批量自测时每条之间至少间隔 2 秒;真实 hook 事件间隔远大于此,无需修改脚本。
其他要点
claude.exe有一个可见但 0×0 的PseudoConsoleWindow,且在进程链上排在 Windows Terminal 之前。定位窗口时判据必须含有效尺寸并取面积最大者,否则点击毫无反应- 必须用 Windows PowerShell 5.1(
powershell.exe)。New-Object Windows.Data.Xml.Dom.XmlDocument会失败(Cannot find type),应改用GetTemplateContent();pwsh的 WinRT 类型投影不稳定 - 修改 hooks 配置立即生效,无需重启会话(网上多数文章说必须重启,与本机实测不符)
async: true的 hook 不写进会话 transcript,在 transcript 里查不到notify_hook.py不代表没有触发,地面真值是notify.log
已知局限
Windows Terminal 没有公开的「按标签页聚焦」API,只能定位到窗口级。多标签页时,焦点会落在最后点过的那个标签页上。若要精确跳到原标签页,只能改用向该标签页的 ConPTY 写入转义序列,属于另一套方案。
设计原则
- 绝不阻塞 Claude:全部 hook
async: true后台运行 - 绝不 fail-open 出错:任何异常都吞掉,退出码恒为 0
- 定位失败不影响送达:
terminal_target()找不到窗口时返回(0, 0),此时发送不带launch的通知——照常弹出,只是点击不跳转。附加能力不能拖累主功能