在 WSL 中运行 Claude Code 时,很容易错过它等待输入的时刻——尤其是当你切换到其他窗口的时候。本文记录了我搭建的通知系统:当 Claude 停止响应或请求权限时,会弹出 Windows 气泡通知,点击通知可直接聚焦到对应的 WezTerm 面板。
工作原理#
Claude Code 在 ~/.claude/settings.json 中提供了钩子(hooks)系统,其中两个事件特别有用:
Stop— Claude 完成一次响应、等待用户输入时触发。钩子载荷包含last_assistant_message、cwd和transcript_path。PermissionRequest— Claude 需要批准才能运行某个工具(Bash 命令、文件写入等)时触发。载荷包含tool_name和tool_input。
两个钩子均以异步方式(async: true)运行 shell 命令,不会阻塞 Claude。
| |
脚本说明#
~/.claude/notify-stop.sh#
从钩子的标准输入 JSON 中提取标题、消息正文和 cwd,然后启动 PowerShell 发送通知:
| |
~/.claude/notify-permission.sh#
结构相同,但正文由工具名称和关键参数拼接而成:
| |
通知示例:Bash: git status 或 Edit: src/main.go。
~/.claude/notify-stop.ps1#
PowerShell 脚本完成四件事:
- 已聚焦则跳过 — 检查目标 WezTerm 窗口是否已是前台窗口,若是则静默退出。
- 显示气泡通知 — 在 Windows 系统托盘显示标题和正文。
- 点击时查找正确面板 — 调用
wezterm.exe cli list --format json,按cwd过滤,获取面板 ID 和窗口标题。 - 激活并提升窗口 —
activate-pane切换到正确标签页,再通过 Win32 API 将窗口置于前台。
难点解析#
环境变量无法跨越 WSL→PowerShell 边界#
无法在 bash 中设置变量后用 $Env:VAR 在 PowerShell 中读取。解决方案:用 jq -n 从 bash 写入 JSON 文件,再在 PowerShell 中用 ConvertFrom-Json 读取。
找到正确的 WezTerm 窗口#
wezterm cli list --format json 以 file:// URI 形式返回每个面板的 cwd(例如 file://pc/home/huyang/workdir)。使用 EndsWith 与钩子中的 cwd(/home/huyang/workdir)匹配:
| |
此查询在点击时执行,而非通知弹出时,以确保获取最新的窗口标题。
窗口标题编码不匹配#
Claude 工作期间,WezTerm 会在活动面板标题前加上 ⠂(盲文点,U+2802)。通过 PowerShell 的 ConvertFrom-Json 读取时,这个字符变成了 Γ£│(UTF-8 字节被误读为 cp1252);而 Win32 的 GetWindowText 返回的是 ?。
解决方案:比较前先裁掉两端字符串的非 ASCII、非字母数字前缀:
| |
SetForegroundWindow 被 Windows 拦截#
Windows 会阻止后台进程抢占前台窗口。keybd_event(Alt) 技巧在某些情况下有效,但更可靠的方案是 AttachThreadInput——在调用 SetForegroundWindow 前,先将当前线程附加到前台窗口的线程:
| |
最终效果#
- Claude 完成响应 → 弹出 Windows 通知,显示最后一条消息及所属项目
- Claude 请求权限 → 通知显示具体命令或文件路径
- 点击任意通知 → 聚焦到正确的 WezTerm 窗口和面板
- 若目标窗口已在前台,则不显示通知