1. 项目缘起:从“默默结束”到“主动通知”
不知道你有没有过这样的经历:在终端里跑一个耗时比较长的脚本,比如编译一个大项目、处理一批数据,或者像我一样,用 Claude Code 执行一个复杂的代码生成任务。然后你就切到浏览器去查资料,或者打开另一个编辑器写点别的。过一会儿,你突然想起来:“诶,刚才那个任务跑完了吗?”切回终端一看,哦,早就结束了,进度条停在 100% 那里,安安静静的,仿佛什么都没发生过。
这种“静默结束”在很多时候其实挺耽误事的。尤其是当任务结果是你下一步工作的输入时,你可能会白白等上几分钟甚至更久。对于 Claude Code 这种 AI 编程助手来说更是如此,它处理一个复杂请求可能需要几十秒,这段时间足够你走个神、回个消息,然后就把这事儿给忘了。
所以,我就想,能不能让 Claude Code 在任务结束时,像老朋友一样“喊”我一声?不是那种弹个对话框需要我去点的干扰,而是一个简单的、非侵入式的提示音,告诉我:“嘿,伙计,你交代的事儿办妥了,可以回来验收了。”
这个需求听起来简单,但实现起来却让我把 Claude Code 的 Hooks 机制、操作系统的进程间通信,以及不同 Shell 环境下的脚本编写都摸了一遍。最终,我找到了一个既优雅又通用的解决方案:利用 Claude Code 的Stop Hook,配合系统原生的音频播放能力,实现任务结束自动播报。整个过程不依赖任何第三方播放器软件,几行脚本就搞定,而且跨平台(Windows/macOS/Linux)的思路是相通的。下面,我就把这套“让 Claude Code 会说话”的配置方法,以及背后的原理和踩过的坑,详细分享给你。
2. 理解 Claude Code 的 Hooks 机制:事件驱动的扩展点
要实现自动提示音,首先得明白 Claude Code 在哪里给我们留了“后门”。这个后门就是Hooks(钩子)。简单来说,Hooks 是 Claude Code 在自身生命周期的关键节点(比如启动、收到消息、结束任务等)向外抛出的“事件”。我们可以为这些事件编写自定义脚本,当事件发生时,Claude Code 就会自动执行我们的脚本。
这有点像我们熟悉的 Git Hooks(比如pre-commit),或者一些 CI/CD 工具(如 Jenkins)的构建后步骤。Claude Code 的 Hooks 让我们能够深度定制它的行为,而不需要去修改其核心代码。
Claude Code 主要支持以下几种类型的 Hooks:
- Start Hook: 在 Claude Code 启动时执行。
- Message Hook: 在 Claude Code 发送或接收消息时执行。这通常用于消息的格式化、日志记录或触发外部工作流。
- Stop Hook:这是我们本次关注的重点。在 Claude Code 结束一个任务(例如,完成一次代码生成、执行完一个命令)时执行。
- Error Hook: 在 Claude Code 运行过程中发生错误时执行。
我们的目标很明确:在Stop Hook被触发时,执行一个播放提示音的脚本。这样,每当 Claude Code 完成你交给它的任何一项工作,你都能立刻得到听觉反馈。
注意:Hooks 脚本的执行是同步的(或者说,会阻塞 Claude Code 的主线程直到脚本执行完毕)。因此,你的 Hook 脚本应该尽可能轻量和快速,避免长时间的操作,否则会影响 Claude Code 本身的响应速度。播放一个短促的提示音是完美符合这个要求的。
3. 方案选型:为什么不用“轮询”而用“事件”
在动手之前,我们不妨先想想有没有其他“土办法”。比如,写个脚本轮询检查 Claude Code 的进程状态?或者,在运行命令时手动在后面加个&& echo -e '\a'(发送系统蜂鸣)?
这些方法都有明显的缺陷:
- 轮询(Polling)效率低下且不精确:你需要启动一个额外的后台进程,每隔几秒去检查一次,浪费系统资源。更重要的是,你很难精准定义“任务结束”的时刻。是 Claude Code 进程退出?还是某个特定会话结束?轮询很难把握这个粒度。
- 手动附加命令不通用:你需要在每次执行命令时都记得加上播放声音的指令,这违背了我们“自动化”的初衷。而且,对于 Claude Code 内部触发的复杂任务链,你根本无法手动干预。
因此,基于Stop Hook的事件驱动方案是唯一正确、优雅的选择。它精准地在“任务结束”这个业务定义明确的事件点上触发我们的动作,无需轮询,无需手动干预,与 Claude Code 的生命周期完美绑定。
4. 核心实现:编写跨平台的提示音播放脚本
确定了使用 Stop Hook 后,接下来要解决的核心问题是:如何在 Hook 脚本里播放一个提示音?
我们追求的是零依赖、系统原生。好消息是,主流操作系统都提供了命令行播放音频的基础能力。
4.1 Windows 平台:基于 PowerShell 的System.Media.SoundPlayer
在 Windows 上,最原生的方式是使用 .NET Framework 或 .NET Core 中的System.Media.SoundPlayer类。我们可以通过 PowerShell 直接调用它,无需安装任何额外软件。
首先,我们需要一个提示音文件。系统自带的“叮”声是一个好选择,它的路径通常是C:\Windows\Media\notify.wav。你也可以使用任何其他.wav格式的简短音频文件。
下面是一个完整的 PowerShell 脚本 (play_notify.ps1):
# play_notify.ps1 # 使用 .NET 的 SoundPlayer 播放系统提示音 # 定义音频文件路径,这里使用系统自带的提示音 $soundPath = “C:\Windows\Media\notify.wav” # 检查文件是否存在 if (Test-Path $soundPath) { # 加载 .NET 程序集(如果尚未加载) Add-Type -AssemblyName System.Windows.Forms # 创建 SoundPlayer 对象并播放 $player = New-Object System.Media.SoundPlayer $soundPath $player.PlaySync() # PlaySync 会同步播放,即等待播放完毕脚本才结束。对于短提示音没问题。 # 如果想要异步播放(不阻塞),可以使用 $player.Play() } else { Write-Warning “提示音文件未找到: $soundPath” # 备选方案:使用控制台蜂鸣符(不一定所有终端都支持) # Write-Host “`a” -NoNewline }脚本要点解析:
Add-Type -AssemblyName System.Windows.Forms:这行代码确保了包含System.Media.SoundPlayer的程序集被加载到当前的 PowerShell 会话中。这是关键一步。PlaySync()方法:同步播放,脚本会等待声音播放完毕后再退出。对于我们的场景(短提示音)是合适的,能确保声音被完整听到。如果你担心可能存在的极小延迟,可以使用Play()进行异步播放,但需注意 Hook 脚本可能提前结束。- 备选蜂鸣方案:脚本中注释了
Write-Host “a”`,这是发送 ASCII 码中的 BEL(响铃)字符。在某些终端(如老版 CMD)中会触发系统蜂鸣声,但在现代终端(如 Windows Terminal、VS Code 内置终端)或 PowerShell 默认配置下可能无效,因此仅作为备选。
4.2 macOS 和 Linux 平台:使用afplay或paplay、aplay
在类 Unix 系统上,播放音频的命令行工具很丰富。
对于 macOS:系统自带afplay命令,非常简单。
#!/bin/bash # play_notify_mac.sh afplay /System/Library/Sounds/Ping.aiff # 或者使用其他系统声音,如 Glass.aiff, Submarine.aiff 等对于 Linux:情况稍复杂,因为音频系统多样(ALSA, PulseAudio, PipeWire)。但通常可以尝试以下命令:
paplay(PulseAudio): 目前大多数桌面 Linux 发行版的首选。#!/bin/bash # play_notify_linux.sh # 尝试播放系统提示音,路径可能因发行版而异 SOUND_FILE=“/usr/share/sounds/freedesktop/stereo/complete.oga” if [ -f “$SOUND_FILE” ]; then paplay “$SOUND_FILE” elif command -v paplay &> /dev/null; then # 如果找不到文件,但 paplay 命令存在,可以播放一个简短的内置提示(可能需要生成) echo -e ‘\a’ # 先尝试控制台蜂鸣 # 更可靠的方式:用 sox 或生成一个简单波形,这里以简单蜂鸣为例 paplay --volume=32768 <(echo -e ‘#! /usr/bin/env bash\ncat /dev/urandom | head -c 100 | aplay -q 2>/dev/null &’) fiaplay(ALSA): 更底层的音频驱动。# 播放一个 WAV 文件 aplay -q /usr/share/sounds/alsa/Noise.wav # 或者播放一个生成的简单声音(例如 1kHz 正弦波 0.1秒) # 需要安装 `sox` 工具包:`sudo apt install sox` (Debian/Ubuntu) # play -q -n synth 0.1 sin 1000- 控制台蜂鸣
echo -e ‘\a’:最原始的方法,依赖于终端模拟器和系统配置,在很多现代桌面环境下可能无声。
Linux 下的实操建议: 由于 Linux 桌面环境碎片化,最稳健的方法是:
- 首选检查并安装
libcanberra或sound-theme-freedesktop:它们提供了标准化的声音主题和播放命令canberra-gtk-play。sudo apt install libcanberra-gtk-module libcanberra-gtk3-module sound-theme-freedesktop # Debian/Ubuntu canberra-gtk-play -i complete - 将播放命令封装在脚本中,并做好失败静默处理:因为 Hook 脚本不应该因为播放失败而报错,导致 Claude Code 本身异常。
#!/bin/bash play_sound() { # 尝试多种方法,直到一个成功 if command -v canberra-gtk-play &> /dev/null; then canberra-gtk-play -i complete 2>/dev/null && return 0 fi if command -v paplay &> /dev/null; then paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null && return 0 fi echo -e ‘\a’ 2>/dev/null # 最后尝试蜂鸣 return 0 } play_sound
5. 配置 Claude Code 的 Stop Hook
编写好播放脚本后,接下来就是告诉 Claude Code 使用它。Claude Code 的配置通常位于用户目录下的一个配置文件中,例如~/.config/claude-code/config.json(Linux/macOS)或%APPDATA%\claude-code\config.json(Windows)。具体路径请参考 Claude Code 的官方文档。
我们需要在配置文件中添加hooks字段,并指定stop钩子为我们脚本的路径。
5.1 配置文件示例
假设我们的播放脚本放在C:\Users\YourName\scripts\play_notify.ps1(Windows)或/home/yourname/scripts/play_notify.sh(macOS/Linux)。
Windows (config.json) 配置:
{ “model”: “claude-3-5-sonnet”, “api_key”: “your-api-key”, “hooks”: { “stop”: “powershell -ExecutionPolicy Bypass -File C:\\Users\\YourName\\scripts\\play_notify.ps1” } }关键点:
- 我们使用
powershell -ExecutionPolicy Bypass -File ...来执行 PowerShell 脚本。-ExecutionPolicy Bypass是为了绕过默认可能限制脚本执行的安全策略,确保脚本能运行。 - 路径中的反斜杠需要转义,所以是
C:\\Users\\...。
macOS/Linux (config.json) 配置:
{ “model”: “claude-3-5-sonnet”, “api_key”: “your-api-key”, “hooks”: { “stop”: “/home/yourname/scripts/play_notify.sh” } }关键点:
- 确保脚本文件有可执行权限:
chmod +x /home/yourname/scripts/play_notify.sh。 - 在脚本内部,我们已经处理了不同播放命令的兼容性。
5.2 配置生效与测试
保存配置文件后,你需要重启 Claude Code(如果它正在运行)以使新的 Hook 配置生效。
测试方法非常简单:在 Claude Code 中执行任何一个命令,等待其完成。如果配置正确,你应该能在任务结束后立刻听到设定的提示音。
6. 进阶技巧与避坑指南
在实际配置和使用过程中,我遇到了几个典型问题,这里分享出来帮你避坑。
6.1 路径与权限问题:脚本无法执行
这是最常见的问题。Hook 配置中指定的脚本路径必须是绝对路径,并且运行 Claude Code 的用户必须有权限读取和执行该脚本。
- 症状:Claude Code 任务结束后没有声音,查看 Claude Code 的日志(如果有)或系统事件查看器,可能会发现“文件未找到”或“权限被拒绝”的错误。
- 排查:
- 检查路径:在终端或文件管理器中手动导航到配置文件里写的路径,看看文件是否存在。特别注意 Windows 的路径分隔符和转义。
- 检查权限(Linux/macOS):在终端执行
ls -l /path/to/your/script.sh,确认你有执行 (x) 权限。如果没有,使用chmod +x命令添加。 - 手动测试脚本:在对应的 Shell 环境中(PowerShell 或 Bash),手动运行你的脚本命令,看是否能正常播放声音。这是最直接的验证方式。
6.2 脚本执行超时或阻塞
如前所述,Hook 脚本是同步执行的。如果你的脚本执行时间过长(比如网络请求、复杂计算),会拖慢 Claude Code。
- 症状:Claude Code 在任务结束后会“卡住”一会儿才返回提示符。
- 解决:
- 确保播放是异步的:在 PowerShell 中,可以考虑使用
$player.Play()而非PlaySync(),但要注意脚本可能立即退出导致声音中断。一个更稳妥的方法是启动一个极短的后台作业。# 在 play_notify.ps1 中 $player = New-Object System.Media.SoundPlayer “C:\Windows\Media\notify.wav” $player.Play() # 异步播放 Start-Sleep -Milliseconds 100 # 稍作等待,确保播放启动 # 脚本结束,但声音播放进程独立继续 - Linux/macOS 使用
&放入后台:在 Bash 脚本中,可以在播放命令后加&将其放入后台。paplay /usr/share/sounds/freedesktop/stereo/complete.oga & - 核心原则:Hook 脚本的逻辑必须极其轻量,播放声音这种 I/O 操作应尽快启动并移交系统处理,脚本自身应迅速退出。
- 确保播放是异步的:在 PowerShell 中,可以考虑使用
6.3 环境变量与上下文差异
Claude Code 在运行 Hook 脚本时,其环境变量(如PATH)可能与你在交互式终端中看到的不同。这可能导致脚本中使用的命令(如afplay,paplay)找不到。
- 症状:手动运行脚本正常,但通过 Claude Code Hook 触发时失败。
- 解决:
- 使用绝对路径:在脚本中,对于关键的系统命令,使用其绝对路径(例如
/usr/bin/afplay而不是afplay)。你可以通过which afplay命令来查找绝对路径。 - 在脚本中设置 PATH:在脚本开头显式设置
PATH环境变量,包含必要的目录。#!/bin/bash export PATH=“/usr/bin:/bin:/usr/local/bin:$PATH” # ... 其余脚本内容 - 简化依赖:这正是为什么我们优先选择系统原生 API(如 PowerShell 的 .NET 调用)或最普遍存在的工具的原因。
- 使用绝对路径:在脚本中,对于关键的系统命令,使用其绝对路径(例如
6.4 音频输出设备与音量
有时脚本执行成功,但你就是听不到声音。
- 排查:
- 检查系统音量:确保系统音量未静音,且音量大小合适。
- 检查默认播放设备:特别是 Windows 和 Linux,有时音频输出可能被定向到了错误的设备(如一个未插耳机的端口)。
- 测试其他声音:播放一个音乐文件或视频,确认系统音频输出正常。
- 查看脚本错误输出:修改你的脚本,将可能出现的错误信息重定向到一个日志文件,以便排查。
# PowerShell 示例,将错误和信息输出到日志 Start-Transcript -Path “C:\Temp\claude_hook.log” -Append # ... 你的播放代码 ... Stop-Transcript
7. 扩展思路:让提示更智能
基本的提示音实现了,但我们可以做得更好。Stop Hook 的脚本可以接收 Claude Code 传递的一些上下文信息(具体取决于 Claude Code 的实现,请查阅其最新文档)。理论上,你可以根据任务的成功或失败状态,播放不同的提示音。
例如,你可以设想:
- 如果任务成功退出(退出码为0),播放一个清脆的“完成”音。
- 如果任务因错误退出(退出码非0),播放一个低沉的“错误”音。
这需要你的播放脚本能够读取到任务结束的状态码。如果 Claude Code 的 Stop Hook 支持传递退出状态(例如通过环境变量或命令行参数),你的脚本就可以据此做出判断。
#!/bin/bash # 假设 Claude Code 通过环境变量 $CLAUDE_EXIT_CODE 传递退出码 EXIT_CODE=${CLAUDE_EXIT_CODE:-0} # 默认为0 if [ “$EXIT_CODE” -eq 0 ]; then paplay /usr/share/sounds/freedesktop/stereo/complete.oga else paplay /usr/share/sounds/freedesktop/stereo/dialog-error.oga fi即使 Claude Code 当前不直接提供,你也可以通过包装 Claude Code 的调用命令,在外部捕获其退出状态,然后触发不同的声音。这需要更深入的集成,但思路是可行的。
8. 总结与个人体会
通过配置一个简单的 Stop Hook,我们成功让 Claude Code 从“沉默的劳动者”变成了“会打招呼的伙伴”。这个看似微小的改进,在实际开发中带来的体验提升是显著的。它减少了上下文切换的成本,让你能更流畅地在多个任务间并行。
回顾整个实现过程,最关键的是理解“事件驱动”的思想。与其去笨拙地轮询或手动干预,不如利用工具本身提供的扩展点。Claude Code 的 Hooks 正是这样一个强大的扩展点。
在具体实现上,追求“系统原生”和“零依赖”极大地增强了方案的可靠性和可移植性。无论是 Windows 的 PowerShell + .NET,还是 macOS 的afplay,或是 Linux 下对多种音频系统的兼容处理,目标都是让脚本在任何一台新机器上都能以最小成本运行起来。
最后,关于配置细节,绝对路径、执行权限和环境上下文这三个点是 Hook 脚本失败的重灾区,务必在测试阶段仔细验证。一个良好的习惯是,先在独立的 Shell 环境中完整地走一遍你的脚本逻辑,确保无误后再集成到 Claude Code 的配置中。
我个人现在已经在所有的工作机上配置了这个功能,它已经成了我使用 Claude Code 时一个不可或缺的“背景感官”。当那声熟悉的提示音响起,我就知道,又可以继续下一步了。这种无缝的、自动化的反馈,正是高效工作流中那些让人愉悦的细节之一。