Claude

Claude Code 长任务不让 Mac 睡眠:心跳加看门狗脚本方案

编辑部阅读约 7 分钟

用 Claude Code 跑长任务时 Mac 中途睡眠,任务被冻断。文章梳理空闲睡眠与合盖睡眠的区别,对比 caffeinate、GUI 工具、提示词里执行 pmset、hooks 加看门狗等做法,给出可落地的配置步骤、检查清单与已知边界,包括多会话、孤儿清理和过热保护的处理。

用 Claude Code 跑重构、批量改文件、长时间跑测试这类任务,人往往要离开电脑。回来一看,任务停在半路——不是模型卡住,而是 Mac 先睡了。系统一进入睡眠,进程被冻结、网络断开,正在进行的 API 请求大概率超时,那一轮工具调用就废了。

直接用 caffeinate 能顶一阵,但它不区分"任务在跑"和"任务已经跑完",容易一直耗电;而且合盖照样睡。更合理的做法是让机器只在真正有活的时候保持唤醒,活干完就交还给系统正常休眠。下面这套思路来自一个跑了一个多月的实践:用 Claude Code 的 hooks 维护"活跃会话"状态文件,再用一个常驻的看门狗根据状态决定是否阻止睡眠。

先分清两种睡眠和几种场景

讨论里最典型的分歧是"我用 CLI 没这问题"和"我合盖就断了"。两者都可能对,因为触发条件不同:

  • 空闲睡眠:机器一段时间无输入就睡。长时间无人值守跑任务,命中的就是它。
  • 合盖睡眠:合上盖子立即睡,与是否有输入无关。外出办公时最常踩。
  • 短任务:几分钟内跑完,还没到系统睡眠阈值,所以"看起来没问题"。

另一个容易被忽略的点是客户端形态。睡眠是操作系统层面的行为,跟用桌面客户端还是命令行无关;差别只在于睡眠发生时,进程里是否有正在等待网络返回的请求。因此"我这边没断过"通常只是任务不够长,不能当成不需要处理。

几种常见做法与取舍

做法 防空闲睡眠 防合盖睡眠 多会话 异常时自动恢复 适合场景
caffeinate 手动开 ✅ ❌ ✅ ❌ 需手动关 临时跑一次
caffeinate 包裹进程 ✅ ❌ ✅ ✅ 进程退出即释放 一条命令跑到底
Amphetamine 等 GUI 工具 ✅ ✅(需配置) 一般 依赖触发器 不想写脚本
提示词里让模型执行 pmset ✅ ✅ ❌ ❌ 崩溃就钉住 单人单窗口、偶尔一用
hooks + 看门狗 ✅ ✅ ✅ ✅ 长期高频使用
换成台式常驻机器 ✅ 不涉及 ✅ — 任务长期在线

最省事的第一步:用 caffeinate 包裹

如果只是偶尔跑一次,别急着上脚本:

caffeinate -i -m claude

caffeinate 会跟子进程绑定,Claude 退出后自动释放,不会留下"永远不睡"的残留。常用参数:-i 阻止空闲睡眠,-m 阻止磁盘休眠,-d 阻止显示器睡眠,-t 3600 定时释放,-u 模拟用户活动。

局限很明确:它管不了合盖。macOS 上能压住合盖睡眠的是 pmset -a disablesleep 1(执行前需要管理员权限,用完记得改回 0)。另外 caffeinate 也不知道任务是否真的在干活,包裹整个会话时,即使你在发呆它也让机器醒着。

直接在提示词里写 pmset 的问题

"让模型在任务开始时执行 disablesleep 1、结束时执行 disablesleep 0"确实可行,但只适合单人单窗口的临时场景。一旦把条件放宽就会露馅:

  • 需要提权,每次输密码或配置免密都麻烦;
  • 模型中途报错、进程被杀、终端关掉,收尾那一步不会执行,机器被永远钉在"不睡"状态;
  • 多开几个窗口时,A 任务结束时把开关关掉,B 任务还在跑;
  • 没有任何温度判断;
  • 提示词写多了模型可能忘记或改写命令。

真正麻烦的不是"怎么关睡眠",而是"什么条件下该关、什么时候必须关回来"。这正是脚本要解决的。

核心方案:心跳 + 看门狗

把问题拆成两半:谁在干活由 Claude Code 的 hooks 记录;要不要阻止睡眠由一个独立的常驻进程决策。两边通过一个状态文件解耦,互不干扰。

第一半:心跳,把"正在干活"变成文件时间戳

思路是提交 prompt 时、每次工具调用后,更新一个会话专属的时间戳文件;回合结束时删除。文件"新鲜"就代表这个会话在干活。

把下面的脚本放到固定目录(例如 ~/.claude/keepawake/):

#!/bin/bash
# touch.sh —— 刷新当前会话的心跳文件
DIR="$HOME/.claude/keepawake/active"
mkdir -p "$DIR"
KEY="${CLAUDE_SESSION_ID:-$(ps -o tty= -p $PPID | tr -d ' ')}"
date +%s > "$DIR/$KEY"
#!/bin/bash
# clear.sh —— 回合结束,清掉本会话心跳
DIR="$HOME/.claude/keepawake/active"
KEY="${CLAUDE_SESSION_ID:-$(ps -o tty= -p $PPID | tr -d ' ')}"
rm -f "$DIR/$KEY"

然后在 Claude Code 的配置文件(通常是 ~/.claude/settings.json)里挂到 hooks 上:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "$HOME/.claude/keepawake/touch.sh" } ] }
    ],
    "PostToolUse": [
      { "hooks": [ { "type": "command", "command": "$HOME/.claude/keepawake/touch.sh" } ] }
    ],
    "Stop": [
      { "hooks": [ { "type": "command", "command": "$HOME/.claude/keepawake/clear.sh" } ] }
    ]
  }
}

几点务必先确认:hook 事件名、配置结构和可用环境变量以官方文档为准(不同版本可能有调整);会话标识不要用固定文件名,否则多开窗口会互相覆盖。如果拿不到会话 ID,用终端 TTY 或工作目录哈希做 key 也能凑合。

第二半:看门狗,每 20 秒做一次决策

看门狗是个常驻的 LaunchAgent,按固定间隔扫描状态目录,逻辑大致是:

#!/bin/bash
DIR="$HOME/.claude/keepawake/active"
STALE=$((30 * 60))          # 30 分钟没刷新视为孤儿
now=$(date +%s)
active=0

for f in "$DIR"/*; do
  [ -e "$f" ] || continue
  ts=$(cat "$f" 2>/dev/null || echo 0)
  if [ $((now - ts)) -gt $STALE ]; then rm -f "$f"; continue; fi
  active=1
done

# 过热保护:CPU_Speed_Limit 小于 100 说明在降频
hot=0
limit=$(pmset -g therm | awk '/CPU_Speed_Limit/ {print $3}')
[ -n "$limit" ] && [ "$limit" -lt 100 ] && hot=1

if [ $active -eq 1 ] && [ $hot -eq 0 ]; then
  pmset -a disablesleep 1
else
  pmset -a disablesleep 0
fi

对应的 LaunchAgent(放在 ~/Library/LaunchAgents/)关键字段:

<key>ProgramArguments</key>
<array><string>/usr/local/bin/keepawake-watchdog.sh</string></array>
<key>StartInterval</key><integer>20</integer>
<key>RunAtLoad</key><true/>

权限是这个方案最容易翻车的地方:pmset -a disablesleep 需要 root,而 LaunchAgent 以当前用户身份运行。要么改用 LaunchDaemon(系统级、以 root 运行),要么为这条命令配一条范围极窄的 sudo 规则。两条路都是改系统安全配置,动手前先确认清楚影响,改完用 sudo pmset -g 之类的方式验证当前状态。

三个必须补上的兜底

  1. 孤儿清理:状态文件超过阈值(示例是 30 分钟)没刷新就删掉。这样 Claude 崩溃、终端被直接关掉、机器重启后,看门狗下一轮就会把 disablesleep 翻回 0。这是整套方案里最重要的一条,没有它就是在赌运气。
  2. 多会话互不干扰:一个文件对应一个会话,只要有任意一个新鲜就保持唤醒;只有全部清空或过期才恢复睡眠。
  3. 过热放行:整夜不合盖、任务又在满负荷跑,很容易积热。除了上面的降频判断,还可以限制总时长(例如连续唤醒超过 N 小时强制放行一次),或者在用电池时完全不启用。

落地检查清单

  • 先手动跑一次 pmset -a disablesleep 1 / 0,确认权限路径通了
  • 用 pmset -g 或 pmset -g custom 核对 disablesleep 的实际值
  • hooks 触发后,ls -l ~/.claude/keepawake/active/ 里能看到文件在刷新
  • 开两个窗口各跑一个任务,关掉其中一个,确认另一个仍在保持唤醒
  • 直接 kill 终端模拟崩溃,确认 30 分钟后状态自动恢复
  • 拔掉电源试一次,确认电池模式下行为符合预期
  • 合盖放包里前想一下散热,别让机器在密闭环境里满负荷跑一夜

已知边界与替代思路

  • 长时间单条命令有盲区:hooks 只在工具调用边界触发,一条跑 40 分钟的构建命令期间没有事件。可以在 PreToolUse 也刷新心跳,代价是卡死的命令同样会被当作"在干活";更稳妥的是对这类命令单独用 caffeinate 包裹。
  • 非交互/后台启动的场景需要自行验证 hooks 是否照常触发,不要想当然。
  • 睡眠只是中断原因之一:合盖导致 Wi-Fi 断开、网络抖动、代理掉线,同样会让工具调用失败。即使机器不睡,网络不稳也照样断。
  • 不想维护脚本的话,GUI 工具或直接用 caffeinate 包裹也能凑合,代价是缺少按需恢复和异常兜底。
  • 台式机思路:Mac mini 没有合盖问题,可以设成长期不休眠,把长任务放到固定机器上跑。如果本地笔记本既要移动又要跑长任务,租一台常驻机房的远程 Mac 作为补充也是可选做法——整机放在机房、按月使用、远程连接,网络环境和在线时长都更可控。

小结

这件事的难点不在"怎么阻止睡眠",而在"怎么保证它一定会恢复"。把状态判断从提示词里搬出来,交给 hooks 和独立看门狗,好处是:模型忘不忘、进程崩不崩,都不影响最终的睡眠恢复;坏处是多了一个需要维护的小系统,还要处理权限和过热。先用 caffeinate 过渡,确认自己真的高频跑长任务,再上心跳加看门狗,是比较划算的顺序。

涉及系统电源管理和安全配置的命令,不同 macOS 版本行为可能有差异,落地前请以官方最新文档为准。

Claude Code长任务macOS 自动化pmsethooks