让 Claude Code 跑完长任务后自动在微信通知我
我最近用 Claude Code 的时间比较多。它处理一些稍微复杂一点的事情时,经常要跑十几二十分钟,比如重构一个模块、跑一遍检查,或者批量修改文件。
这段时间如果一直盯着屏幕,其实很浪费;但如果去做别的,也很容易忘记它。等我想起来再切回去,任务可能已经结束很久了。
所以我想做一个很简单的东西:任务跑得久一点,结束后在微信里通知我。通知不用写很多,只要三句话说明这次做了什么就够了。短任务就算了,没必要一直打扰我。
这套东西刚刚做好,还没有经过很长时间的实际使用。不过实现起来并不复杂:在任务开始时记下时间,结束时算一下耗时;如果超过阈值,就让另一个 Claude 根据会话记录写一小段摘要,再借助邮件工具把它送进微信通知里。
先在开始和结束时各做一件事
Claude Code 有 Hook 这个机制,可以在一些生命周期事件发生时执行 shell 命令。我这里实际只用了两个:
UserPromptSubmit:我发出一条消息时,记录开始时间。Stop:Claude 完成这一轮回答时,计算耗时,决定要不要通知我。
看起来有点像打卡。进来时记一下时间,出去时再结算。
我在 ~/.claude/settings.json 里加了两个 Hook:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash \"/Users/you/.claude/hooks/task-summary-start.sh\"",
"timeout": 10
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash \"/Users/you/.claude/hooks/task-summary-mail.sh\"",
"timeout": 120
}
]
}
]
}
}
Stop 的超时我留了 120 秒。因为它后面还要启动一次 Claude 来写摘要,时间留得少了不太放心。
开始时只存一个时间戳
每次 Hook 都是独立的进程,stdin 会收到 JSON,里面有 session_id、transcript_path 之类的信息。也就是说,不能在开始的脚本里放一个 shell 变量,再指望结束的脚本能读到。
我就直接把时间写到临时文件里,并且用 session_id 区分不同会话:
#!/usr/bin/env bash
# UserPromptSubmit Hook:记录任务开始时间,用 session_id 做 key
set -uo pipefail
# 递归锁,后面会用到
if [ -n "${CLAUDE_TASK_SUMMARY_LOCK:-}" ]; then exit 0; fi
input=$(cat)
sid=$(printf '%s' "$input" | jq -r '.session_id // "unknown"')
statedir="${TMPDIR:-/tmp}/claude-task-summary"
mkdir -p "$statedir"
date +%s > "$statedir/${sid}.start"
结束时就从同一个文件里读出开始时间。这样即使同时开了多个会话,也不会把时间算串。
结束时决定要不要通知
结束脚本的事情也不多:先算耗时,短任务直接退出;够久的话,生成摘要,再把它送到微信。
我默认把阈值设成三分钟:
TO="${CLAUDE_TASK_SUMMARY_TO:-me@example.dev}"
THRESHOLD="${CLAUDE_TASK_SUMMARY_THRESHOLD:-180}" # 秒,默认 3 分钟
startfile="$statedir/${sid}.start"
[ -f "$startfile" ] || exit 0
start=$(cat "$startfile")
rm -f "$startfile" # 读完即删,为下一轮重置
elapsed=$(( $(date +%s) - start ))
[ "$elapsed" -lt "$THRESHOLD" ] && exit 0 # 短任务不通知
这个阈值对我来说很重要。平时问几个问题、改一个小地方,几秒钟就结束了;如果这些也发微信通知,最后只会变成不看的垃圾通知。收件人和阈值都放在环境变量里,需要时可以临时调整。
让另一个 Claude 看记录,写三句话
会话的完整记录在 transcript_path 指向的 JSONL 文件里。任务结束后,我取最后一段记录,交给非交互模式的 Claude:
prompt='下面是一段 Claude Code 会话记录的结尾。请用与用户对话相同的语言,
用恰好三句话总结本次任务完成了什么。只输出这三句话,不要前言、不要 markdown。'
summary=$(tail -c 200000 "$transcript" \
| CLAUDE_TASK_SUMMARY_LOCK=1 claude -p "$prompt")
claude -p 是一次性运行的非交互模式,输出可以直接存进变量。这样微信通知里的内容来自实际会话,而不是脚本凭空猜的。
我一开始觉得这件事有一点绕:让 Claude 总结 Claude 做的事。但实际上很顺手。任务本身再复杂,通知里我只需要知道它有没有结束、做了哪几件事,剩下的回到会话里再看就行。
微信才是最终的通知,邮件只是工具
我最后没有用专门的推送服务,而是用了 Agent QQ。收件箱是我绑定了微信通知的 QQ 邮箱。也就是说,邮件工具只是负责把消息送到 QQ 邮箱;我真正收到的是微信通知。
流程是:脚本调用邮件工具,把摘要发到自己的 QQ 邮箱;邮箱收到邮件后,微信再推送到手机。对我来说,这条链路比较稳定,不需要额外的网络环境;而且微信在手机上的通知通常也比较及时。
我用的邮件 CLI 有一个两阶段确认机制。第一次调用拿到确认令牌,第二次带着令牌才会真正发送:
# 第一次调用:拿到确认令牌
out=$(agently-cli message +send --to "$TO" --subject "$subject" --body "$summary")
ctk=$(printf '%s' "$out" | grep -oE 'ctk_[A-Za-z0-9_-]+' | head -n1)
# 第二次调用:带上令牌,真正发出
[ -n "$ctk" ] && agently-cli message +send --to "$TO" --subject "$subject" \
--body "$summary" --confirmation-token "$ctk"
邮件工具不一定要和我一样。换成 sendmail、msmtp,或者自己的 SMTP 脚本也都可以,核心只是把最终的摘要送进自己能及时看到的微信通知里。
有几个地方比较容易出问题
不加锁会无限递归
上面用来写摘要的 claude -p,本身也是一次 Claude Code 运行。它结束时也会触发 Stop Hook;如果不做处理,Hook 又会启动一个 claude -p,然后就会一直套下去。
我用了两层判断:
- 启动子 Claude 时设置
CLAUDE_TASK_SUMMARY_LOCK=1,两个脚本一发现这个变量就直接退出。 - 如果 Hook 输入里的
stop_hook_active是true,也直接退出。
[ -n "${CLAUDE_TASK_SUMMARY_LOCK:-}" ] && exit 0
[ "$(jq -r '.stop_hook_active // false' <<<"$input")" = "true" ] && exit 0
前一个判断负责挡住我主动启动的子 Claude,后一个判断是额外保险。这个问题不太显眼,但如果忘了处理,后果会很明显。
不要让失败悄悄过去
如果摘要没有生成出来,又没有微信通知,我大概只会以为 Claude 还在跑。为了避免这种情况,我做了两个兜底:
- 摘要为空时,还是发一条微信通知,只说任务耗时约多少分钟,自动摘要没有生成,让我回会话里查看。
- 把耗时、是否发送、邮件工具的原始输出写进
$TMPDIR/claude-task-summary/hook.log。
这样至少出现问题时,还有地方可以看。
transcript 里可能有不该发出去的内容
这个方案会让无头 Claude 读取会话记录末尾,再通过邮件工具把摘要发到 QQ 邮箱,最后显示在微信通知里。我目前没有额外做脱敏或内容限制,因为邮件只发到自己的 QQ 邮箱,也可以在 Agent QQ 后台查看实际发出的内容。
不过它并不适合直接照搬到包含密钥、客户资料或其他敏感信息的会话里。这种情况最好先过滤 transcript,限制摘要内容,或者干脆不要启用这条通知。
改完配置要新开会话
Hook 配置是在会话启动时读取的。修改 settings.json 后,当前会话不会立刻生效,要新开一个会话。第一次调试时我就在这里卡了一会儿,一直以为是脚本没有跑起来。
最后
整套脚本大概一百来行 Bash,解决的是一个很小的问题。不过做好以后,我可以把比较长的任务交给 Claude,再去做别的事;等微信响了,再回来看看结果。
它也让我觉得,Hook 还是挺有用的。它不只是拿来跑格式化或者检查代码,也可以把 Claude Code 接进自己原来就在用的工作流里。
AI 说明
本文介绍的需求、实现方式和实际配置由我提供并确认;但文章的主体内容几乎完全由 AI 根据这些材料整理、扩写和改写而成。我只做了事实核对和少量修改。