6个shell,我给 Claude Code 装上了“嘴”

个shell,我给

 

写代码的人多多少少都有过这种瞬间:

AI 在屏幕上哗啦啦输出一大段,你眼睛盯着光标跳,脖子越来越僵,三十分钟后回过神来——刚才到底在看什么?

那天我也是。窗外天黑了,屋里只剩屏幕的光,Claude Code 又在往外吐字。我突然冒出一个念头:

它要是能念给我听就好了

不用多花哨,就像有个人坐在旁边,一边敲代码一边把要紧的话讲出来。这样我可以靠在椅子上闭一会儿眼,可以去倒杯水,可以伸个懒腰——它该说的话我一句不落

我查了一圈,官方没这个功能。但 Claude Code 有个东西叫Hooks,是它在会话开始、结束、每次回答完之后会自动触发的”钩子”。理论上,我可以写几个脚本挂上去,让它每次回答完就调用 macOS 自带的say命令把内容念出来

折腾了一个周末,改了 6 个版本,写了 6 个 Shell 脚本,加起来四百多行。最后真做成了

现在我的 Claude Code 一边写代码一边说话,听上去像电台主播,别说,开始还有点不习惯

这篇文章就把整套配置完整写出来。不会写代码也没关系,我尽量把每一步都讲到能复制粘贴就跑通的程度

你需要先有什么

这套方案目前只在Mac 电脑上能用(Windows 和 Linux 需要改脚本,本文先不展开)

打开你的”启动台”,找到一个叫”终端”的应用——黑黑的,图标像个小窗口。这是后面所有操作要用的地方。不要怕,照着复制粘贴就行

接下来检查两样东西:

第一,你装了 Claude Code 吗?如果在终端里输入claude然后回车,看到提示就说明装了。没装的话先去 Claude 官网装一下,回头再继续

第二,你装了 jq 吗?jq 是个处理 JSON 数据的小工具。在终端里输入:

brewinstalljq

如果提示command not found: brew,说明你电脑上还没有 Homebrew(Mac 上最常用的软件管家),先去 brew.sh 网站照首页那一行命令装一下,再回来执行上面这句

装完之后,准备工作就完成了

第一步:建一个文件夹放脚本

打开终端,复制这一行进去,回车:

mkdir-p~/.claude/hooks

这句话的意思是:在你的用户目录下建一个.claude/hooks文件夹,专门放后面这些脚本

没有任何反应是正常的。终端的哲学就是”没消息就是好消息”,一旦报错它会很大声地告诉你

第二步:写 6 个脚本

下面 6 段命令,一段一段复制进终端,每段回车一次。每一段都是cat > 某个文件 << ‘EOF’开头、EOF结尾的格式——这是 Shell 的一种写法,意思是”把中间这一坨内容原封不动地写进这个文件里”

注意一定要连同最后那个EOF一起复制进去,不然终端会一直等你输完,光标卡在那儿不动

脚本 1:会话启动时叫醒”播音员”

cat > ~/.claude/hooks/tts-start.sh <<‘EOF’#!/bin/bashset-uMONITOR=”$HOME/.claude/hooks/tts-monitor.sh”DEBUG_LOG=”$HOME/.claude/hooks/tts-start-debug.log”log() {echo”$1″>>”$DEBUG_LOG”; }log”=== Starting TTS monitor at$(date)===”VOICE_ENABLED=$(jq -r’.voiceEnabled // false’”$HOME/.claude/settings.json”2>/dev/null ||echofalse)if[“$VOICE_ENABLED”!=”true”];then log”Voice disabled, not starting monitor” exit0fiINPUT=$(cat)TRANSCRIPT_PATH=$(echo”$INPUT”| jq -r’.transcript_path // empty’2>/dev/null)[“$TRANSCRIPT_PATH”=”null”] && TRANSCRIPT_PATH=SESSION_ID=$(echo”$INPUT”| jq -r’.session_id // .conversation_id // .sessionId // .conversationId // empty’2>/dev/null)[“$SESSION_ID”=”null”] && SESSION_ID=[ -z”$SESSION_ID”] && SESSION_ID=”session-$(date +%s)-$”if[ -z”$TRANSCRIPT_PATH”];then PROJECT_DIR=$(echo”$INPUT”| jq -r’.cwd // .workspace_roots[0] // empty’2>/dev/null) [“$PROJECT_DIR”=”null”] && PROJECT_DIR= if[ -n”$PROJECT_DIR”];then TRANSCRIPT_DIR=”$HOME/.claude/projects/$(echo “$PROJECT_DIR” | sed ‘s///-/g’)” SPECIFIC=”$TRANSCRIPT_DIR/$SESSION_ID.jsonl” [ -f”$SPECIFIC”] && TRANSCRIPT_PATH=”$SPECIFIC” fifiPID_SAFE=$(echo”$SESSION_ID”| tr -cd'[:alnum:]-_’| head -c 120)[ -n”$PID_SAFE”] || PID_SAFE=”unknown”PID_FILE=”$HOME/.claude/hooks/tts-monitor-$PID_SAFE.pid”if[ -f”$PID_FILE”];then OLD_PID=$(tr -d’ n'<“$PID_FILE”) if[ -n”$OLD_PID”] &&kill-0″$OLD_PID”2>/dev/null;then kill-TERM”$OLD_PID”2>/dev/null ||true sleep 0.15 kill-KILL”$OLD_PID”2>/dev/null ||true fi rm -f”$PID_FILE”fiSPAWN_LOG=”$HOME/.claude/hooks/tts-monitor-spawn.log”nohup”$MONITOR””$TRANSCRIPT_PATH””$SESSION_ID”>>”$SPAWN_LOG”2>&1 </dev/null &log”Started monitor PID $!”exit0EOF

这是会话开始时第一个被叫醒的脚本。它的工作是检查”语音开关”是不是打开的,然后在后台启动真正干活的”监控员”——也就是下面这一个

脚本 2:真正干活的”监控员”

这是最长的一个,也是最核心的。它负责盯着 Claude Code 的对话记录文件,一旦有新内容就读出来

cat > ~/.claude/hooks/tts-monitor.sh <<‘EOF’#!/bin/bashset-uif[“$#”-ge 2 ];then TRANSCRIPT_PATH=”$1″ [“$TRANSCRIPT_PATH”=”null”] && TRANSCRIPT_PATH= SESSION_ID=”$2″else TRANSCRIPT_PATH=”” SESSION_ID=”unknown”fiPID_SAFE=$(echo”$SESSION_ID”| tr -cd'[:alnum:]-_’| head -c 120)[ -n”$PID_SAFE”] || PID_SAFE=”unknown”PID_FILE=”$HOME/.claude/hooks/tts-monitor-$PID_SAFE.pid”DEBUG_LOG=”$HOME/.claude/hooks/tts-session-$PID_SAFE.log”log() {echo”$1″>>”$DEBUG_LOG”; }VOICE_ENABLED=$(jq -r’.voiceEnabled // false’”$HOME/.claude/settings.json”2>/dev/null ||echofalse)[“$VOICE_ENABLED”!=”true”] &&exit0resolve_by_session() { find”$HOME/.claude/projects”-maxdepth 15 -name”$1.jsonl”-typef 2>/dev/null | head -1}extract_speakable() { echo”$1″| jq -r’ (.message // {}) as $m | ($m.content | if type == “array” then . elif type == “string” then [{“type”:”text”,”text”: .}] else [] end) as $c | ([$c[]? | select(.type == “text”) | .text] | join(” “)) as $t | if ($t | length) > 0 then $t else ([$c[]? | select(.type == “thinking”) | .thinking] | join(” “)) end’2>/dev/null | head -c 500}log”=== Monitor PID $ at$(date)===”echo$ >”$PID_FILE”WAIT_COUNT=0while[“$WAIT_COUNT”-lt 180 ];do [ -n”$TRANSCRIPT_PATH”] && [ -f”$TRANSCRIPT_PATH”] &&break if[ -n”$SESSION_ID”] && [“$SESSION_ID”!=”unknown”];then if[“$((WAIT_COUNT % 3))”-eq 0 ];then RESOLVED=$(resolve_by_session”$SESSION_ID”) if[ -n”$RESOLVED”] && [ -f”$RESOLVED”];then TRANSCRIPT_PATH=”$RESOLVED” break fi fi fi sleep 1 WAIT_COUNT=$((WAIT_COUNT + 1))doneif[ ! -f”$TRANSCRIPT_PATH”];then rm -f”$PID_FILE” exit0filog”Tailing:$TRANSCRIPT_PATH”LAST_LINE=$(wc -l <“$TRANSCRIPT_PATH”| awk'{print $1+0}’)whiletrue;do sleep 0.4 [ ! -f”$TRANSCRIPT_PATH”] &&break CURRENT_LINES=$(wc -l <“$TRANSCRIPT_PATH”2>/dev/null | awk'{print $1+0}’) if[“$CURRENT_LINES”-gt”$LAST_LINE”];then tail -n +$((LAST_LINE + 1))”$TRANSCRIPT_PATH”|whileIFS=read-r line;do ROLE=$(echo”$line”| jq -r’.type // empty’2>/dev/null) if[“$ROLE”=”assistant”];then TEXT=$(extract_speakable”$line”) if[ -n”$TEXT”] && [“${#TEXT}”-le 2000 ];then if!printf’%s’”$TEXT”| grep -q’^“`’;then CLEAN=$(printf’%s’”$TEXT”| sed’s/“`[^`]*“`//g; s/**//g; s/*//g; s/`//g’| tr -d’n’| head -c 320) [ -z”$CLEAN”] && CLEAN=$(printf’%s’”$TEXT”| tr -d’n’| head -c 320) if[ -n”$CLEAN”];then (/usr/bin/say”$CLEAN”2>/dev/null) & fi fi fi fi done LAST_LINE=$CURRENT_LINES fidonerm -f”$PID_FILE”EOF

它会自动过滤掉代码块(不然念起代码来你会想砸电脑),只朗读 Claude “真正要说的”话

脚本 3:会话结束时让”播音员”下班

cat > ~/.claude/hooks/tts-stop.sh <<‘EOF’#!/bin/bashDEBUG_LOG=”$HOME/.claude/hooks/tts-stop-debug.log”echo”=== Stopping at$(date)===”>>”$DEBUG_LOG”INPUT=$(cat 2>/dev/null ||true)SESSION_ID=$(echo”$INPUT”| jq -r’.session_id // .conversation_id // .sessionId // .conversationId // empty’2>/dev/null)if[ -z”$SESSION_ID”] || [“$SESSION_ID”=”null”];then exit0fiPID_SAFE=$(echo”$SESSION_ID”| tr -cd'[:alnum:]-_’| head -c 120)[ -n”$PID_SAFE”] || PID_SAFE=”unknown”PID_FILE=”$HOME/.claude/hooks/tts-monitor-$PID_SAFE.pid”if[ -f”$PID_FILE”];then OLD_PID=$(tr -d’ n'<“$PID_FILE”) if[ -n”$OLD_PID”] &&kill-0″$OLD_PID”2>/dev/null;then kill-TERM”$OLD_PID”2>/dev/null ||true sleep 0.3 kill-KILL”$OLD_PID”2>/dev/null ||true fi rm -f”$PID_FILE”fiEOF

注意它只关掉当前这个窗口的监控,不影响你开着的其他 Claude 窗口。这是踩了好几个坑之后才改成这样的

脚本 4:一键开关语音

cat > ~/.claude/hooks/voice-toggle.sh <<‘EOF’#!/bin/bashSETTINGS_FILE=”$HOME/.claude/settings.json”CURRENT=$(jq -r’.voiceEnabled // false’”$SETTINGS_FILE”)if[“$CURRENT”=”true”];then jq’.voiceEnabled = false’”$SETTINGS_FILE”> /tmp/settings.json.tmp mv /tmp/settings.json.tmp”$SETTINGS_FILE” pkill -f”tts-monitor.sh”2>/dev/null ||true rm -f”$HOME/.claude/hooks/tts-monitor-“*.pid 2>/dev/null ||true killall say 2>/dev/null ||true echo”✓ 语音播报已关闭”else jq’.voiceEnabled = true’”$SETTINGS_FILE”> /tmp/settings.json.tmp mv /tmp/settings.json.tmp”$SETTINGS_FILE” echo”✓ 语音播报已开启”fiEOF

之后想开就开、想关就关,跑一次这个脚本即可

脚本 5:Stop 钩子的“占位脚本”

cat > ~/.claude/hooks/stop-speak.sh <<‘EOF’#!/bin/bashDEBUG_LOG=”$HOME/.claude/hooks/tts-debug.log”VOICE_ENABLED=$(jq -r’.voiceEnabled // false’”$HOME/.claude/settings.json”2>/dev/null ||echofalse)[“$VOICE_ENABLED”!=”true”] &&exit0KILL=$(jq -r’.env.TTS_KILL_ON_STOP // empty’”$HOME/.claude/settings.json”2>/dev/null)if[“$KILL”=”true”] &&command-v killall >/dev/null 2>&1;then killall say 2>/dev/null ||truefiEOF

这个脚本默认什么都不做。别小看它——之前的版本一旦 Claude 暂停就把朗读全部杀掉,结果话还没说完声音就断了,特别恼火。所以这个版本让它对 Stop 视而不见,朗读该念完就念完

脚本 6:自检小工具

cat > ~/.claude/hooks/tts-selftest.sh <<‘EOF’#!/bin/bashset-euo pipefailMON=”$HOME/.claude/hooks/tts-monitor.sh”UUID=”aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee”TMP=$(mktemp /tmp/tts-selftest.XXXXXX)trap”rm -f$TMP$HOME/.claude/hooks/tts-monitor-$UUID.pid 2>/dev/null || true”EXITVOICE=$(jq -r’.voiceEnabled // false’”$HOME/.claude/settings.json”2>/dev/null ||echofalse)if[“$VOICE”!=”true”];then echo”请先开启语音再自检” exit0fiprintf’%sn”{“type”:”user”,”message”:{“role”:”user”,”content”:”ping”}}’>”$TMP””$MON””$TMP””$UUID”&MP=$!sleep 0.8printf’%sn”{“type”:”assistant”,”message”:{“role”:”assistant”,”content”:[{“type”:”text”,”text”:”自检通过”}]}}’>>”$TMP”sleep 2kill-TERM”$MP”2>/dev/null ||trueLOG=”$HOME/.claude/hooks/tts-session-$UUID.log”ifgrep -qE’Tailing|Speakable’”$LOG”2>/dev/null;then echo”OK: 监控正常”else echo”FAIL: 没有抓到内容” tail -20″$LOG”2>/dev/nullfiEOF

这个用来在不开 Claude 的情况下,单独验证脚本本身有没有问题

第三步:给脚本”通行证”

刚写完的脚本是一堆”无权限”的文件,得手动告诉系统这些是可以执行的程序。一行命令搞定:

chmod +x~/.claude/hooks/tts-*.sh~/.claude/hooks/voice-toggle.sh~/.claude/hooks/stop-speak.sh

第四步:让 Claude Code 知道这些脚本的存在

光写完脚本不够,还得告诉 Claude Code:”会话开始时调用这个、结束时调用那个”。这一步要修改它的配置文件

把下面整段复制进终端运行——它会自动帮你改好配置,不需要你手动写 JSON

cp~/.claude/settings.json ~/.claude/settings.json.backup2>/dev/nullpython3<<‘PYTHON_SCRIPT’import json, ossettings_path = os.path.expanduser(‘~/.claude/settings.json’)home = os.path.expanduser(‘~’)try: withopen(settings_path,’r’)asf: settings = json.load(f)except FileNotFoundError: settings = {}settings.setdefault(‘voiceEnabled’, False)settings.setdefault(‘hooks’, {})hooks_to_add = { ‘SessionStart’:f'{home}/.claude/hooks/tts-start.sh’, ‘SessionEnd’: f'{home}/.claude/hooks/tts-stop.sh’, ‘Stop’: f'{home}/.claude/hooks/stop-speak.sh’,}forevent, script in hooks_to_add.items(): settings[‘hooks’].setdefault(event, []) exists= any( h.get(‘hooks’, [{}])[0].get(‘command’,”).endswith(os.path.basename(script)) forh in settings[‘hooks’][event] ) ifnot exists: settings[‘hooks’][event].append({ ‘hooks’: [{‘type’:’command’,’command’: script}] })withopen(settings_path,’w’)asf: json.dump(settings,f,indent=2)print(“✓ 配置完成”)PYTHON_SCRIPT

看到 “✓ 配置完成” 就说明搞定了

第五步:开机仪式

现在所有零件都装好了,但语音开关还是关着的。打开它:

~/.claude/hooks/voice-toggle.sh

终端会回复你✓ 语音播报已开启

然后做一个最朴素的测试:

say”你好,我是 Claude,从今天开始我会念给你听”

如果你听到 Mac 用一种特别人工的腔调把这句话念了出来——恭喜,硬件层面没问题

接着关掉所有 Claude Code 窗口,重新打开一个。这一步很关键,因为 Hooks 配置只在新会话启动时生效

新开一个会话之后,随便问 Claude 一个问题,比如”你好”

如果一切顺利,你会听到它一边在屏幕上打字,一边把内容念了出来

用起来之后你可能想问的问题

怎么换个声音?

Mac 里其实藏着几十种语音。在终端里输入say -v “?”能看到全部清单,里面”Ting-Ting”是普通话女声,”Sin-Ji”是粤语女声,还有日语英语德语各种口音的

想换声音的话,打开~/.claude/hooks/tts-monitor.sh(用 VS Code 或者随便什么文本编辑器),找到/usr/bin/say “$CLEAN”这一行,改成/usr/bin/say -v “Ting-Ting” “$CLEAN”就行

它念得太快/太慢怎么办?

同一行里加个-r参数,后面跟一个数字:/usr/bin/say -r 200 “$CLEAN”。默认 175,越大越快,能调到 500,也能慢到 100

想暂时关掉怎么办?

~/.claude/hooks/voice-toggle.sh

这一句话是开关,再跑一次就重新打开

完全不想要了怎么删?

rm ~/.claude/hooks/tts-*.shrm ~/.claude/hooks/voice-toggle.shrm ~/.claude/hooks/stop-speak.shcp~/.claude/settings.json.backup ~/.claude/settings.json

干净利落

如果它没动静

第一招:看看开关

cat~/.claude/settings.json |grepvoiceEnabled

应该是true。如果是false,跑一次voice-toggle.sh

第二招:自检

~/.claude/hooks/tts-selftest.sh

看终端的输出。”OK”就说明脚本本身没问题,问题在 Claude Code 那一端,重启一下窗口大概率能好

第三招:看日志

ls-t ~/.claude/hooks/tts-session-*.log| head -1| xargs tail -50

这一句会把最新的会话日志最后 50 行打出来。里面如果出现Speakable len=字样,说明监控员抓到内容了,但可能say没跑起来——检查一下系统音量、输出设备是不是被切到了别的设备(比如蓝牙耳机断了之后没回到电脑)

第四招:核武器

pkill-f”tts-monitor.sh”rm -f ~/.claude/hooks/tts-monitor-*.pid

把所有残留进程清空,重新打开 Claude Code 窗口。99% 的疑难杂症这一招就能搞定

写在最后

配好这套东西后,写代码的感觉确实有点不一样

不知道是声音让它多了点什么。屏幕上的字我可以随时划走,但它念出来的时候,我会不自觉地听完。明明内容一样,感觉就是不一样

有时候它在念一段很长的解释,我靠在椅背上,没有去看屏幕,就这么听着。念完了,我才回过神来——哦,刚才在工作

当然,它不会变得更聪明,也不会变得更快。但总觉得屏幕和我之间的那道墙,消失了

总之,你也想体验一下,可以花10分钟给它配上“嘴”,相信我,你会爱上它的

配置文件清单

~/.claude/hooks/

├── tts-start.sh 会话开始时叫醒监控

├── tts-monitor.sh 真正干活的监控员

├── tts-stop.sh 会话结束时让监控下班

├── voice-toggle.sh 一键开关

├──stop-speak.sh Stop 钩子(默认装聋)

└── tts-selftest.sh 不开 Claude 也能自检

觉得有用的话,欢迎点赞转发收藏,遇到任何问题都可以留言或私信,就这样,下期见

作者:秋孝隱

扫一扫 微信咨询

联系我们 青瓜传媒 服务项目

商务合作 联系我们

本文经授权 由青瓜传媒发布,转载联系作者并注明出处:https://www.opp2.com/381295.html

《免责声明》如对文章、图片、字体等版权有疑问,请联系我们广告投放 找客户 找服务 蘑菇跨境
企业微信
运营大叔公众号
运营宝库
运营宝库H5