Claude Code hooks 2.1.141 实战:3 个坑终于修了

Claude Code hooks 2.1.141 修复了后台终端提醒、Shell 参数转义和 PostToolUse 阻断重试三类顽疾。本文详解 terminalSequence、args:[]、continueOnBlock 的配置方式,并附完整 settings.json、环境变量用法和稳妥升级清单。

发布于

Claude Code hooks 2.1.141 实战:3 个坑终于修了

2026 年 Q1 最折腾我的 3 个 Claude Code hooks 问题,悄无声息地在 7 天内陆续修复。第二天一早,我就从管理仓库删掉了 3 段 Shell 临时方案。

Claude Code hooks:一周内修掉 3 个真实问题

2026 年 5 月 6 日至 5 月 13 日,Anthropic 连续发布了 Claude Code 2.1.132 到 2.1.141。这轮更新的大部分改动都集中在 hooks 系统。

我之所以注意到,是因为仓库里正好有 3 处临时补丁,都标着 // TODO: remove when claude-code fixes this。一周之内,这 3 处补丁全被我删掉了。

按造成的损失排序,这 3 个问题分别是:

第一是桌面提醒漏报。我的 Notification hook 会把系统响铃写入 stdout,这样耗时较长的上下文压缩结束后就能提醒我。只有 Claude Code 占用前台 TTY 时它才有效;换成 tmux 分屏、VS Code 集成终端,或焦点落在相邻 WezTerm 窗格时,都会悄无声息地失效。2.1.141 新增的 terminalSequence 解决了这个问题。

第二是 Shell 引号转义失控。我的 Stop hook 执行 bash -lc "node post-stop.js --reason '$CLAUDE_STOP_REASON'",Claude 只要返回一个带撇号的 reason 字符串,命令就会立即出错。2.1.139 引入的 args: string[] exec 形式彻底绕开了这个坑。

第三是 PostToolUse 拒绝操作后直接终止当前轮次,而不是把结果交回 Claude 继续处理。由于运行时把 block 决策当作致命错误,我只能彻底停用一个 schema 校验 hook。2.1.139 新增了 continueOnBlock:它会把 block 转成重试信号,并将 reason 注入上下文。

文末附上完整 settings.json,中间不绕弯子。

90 秒看懂 Claude Code hooks 的工作机制

Claude Code 2.1.x 提供 9 种 hook 事件:SessionStart、PreToolUse、PostToolUse、UserPromptSubmit、Notification、Stop、SubagentStop、PreCompact 和 SessionEnd。每个 hook 都是由运行时启动的一条命令,并接收文档规定的环境变量与 stdin payload。运行时会把 hook 的 stdout 当作 JSON 解析,其中几类字段会直接控制运行行为:decision(allow/deny/block)、reason(回传给 Claude 或展示给用户的字符串)、terminalSequence(写入控制 TTY 的原始字节),以及少量事件专用字段。

最需要记住的一点是:运行时以 hook 的 stdout 为准。无法识别的字段毫无作用;能够识别的字段,则会改变 Claude 下一轮看到的内容。整个机制就是这么简单。

terminalSequence:不占用 TTY 也能收到桌面通知

在 2.1.141 之前,我的 Notification hook 是这样写的:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "bash -lc 'printf \"\\a\" && notify-send \"Claude\" \"$CLAUDE_MESSAGE\"'"
    }]
  }
}

printf "\a" 原本应该让终端响铃。实际情况却是,它只会把 \a 写入 hook 进程继承到的文件描述符;除非 Claude Code 此刻就在前台,否则那个描述符并不是用户的终端。比如 tmux 分屏中,Claude 运行在窗格 2、我在窗格 1 编辑时,铃声根本传不到外层终端。notify-send 虽然有效,但通知会躲进 GNOME 通知托盘,我经常看不到。

2.1.141 在 hook 的 stdout JSON 中增加了 terminalSequence 字段。运行时拿到这个字符串后,会绕过 hook 进程的 stdio,直接写入控制终端设备。升级后的配置如下:

JSON
{
  "hooks": {
    "Notification": [{
      "command": "node hooks/notify.mjs"
    }]
  }
}
JavaScript
import { execSync } from "node:child_process";

const payload = JSON.parse(
  await new Response(process.stdin).text()
);

execSync(`notify-send "Claude" ${JSON.stringify(payload.message)}`);

process.stdout.write(JSON.stringify({
  terminalSequence: ""
}));

 就是 BEL。运行时把它写进控制 TTY,因此即使 Claude 位于后台窗格,外层终端也会响铃。按我的测试,macOS Terminal、iTerm2、WezTerm 和 Alacritty 都能正确处理。tmux 会透传 BEL;考虑到 OSC 52,仍建议在 tmux.conf 中保留 set -g allow-passthrough on。

有个边界情况值得注意:VS Code 集成终端默认会吞掉 BEL。希望它也响铃,需要在用户设置中配置 "terminal.integrated.enableBell": true。

args: string[]:从根源上消除 hook 命令的 Shell 转义

Stop hook 会在一轮任务结束时触发。我用它把会话元数据写入 Cloudflare D1 实例,之后便能用 grep 检索历史运行记录。

2.1.139 之前的版本如下:

JSON
{
  "hooks": {
    "Stop": [{
      "command": "bash -lc \"node scripts/post-stop.js --session $CLAUDE_SESSION_ID --reason '$CLAUDE_STOP_REASON'\""
    }]
  }
}

采用这种写法的第一个月,我就遇到了 3 类转义问题:

$CLAUDE_STOP_REASON 中的撇号会提前闭合单引号参数,剩余内容随即被解析成 Shell token。当 Claude 返回 user's request completed 这样的 reason 时,hook 会崩溃,会话日志也随之丢失。

工具名中的反引号也会出问题。如果 Claude 在回复中写了 `bash`,hook 触发时 $CLAUDE_TOOL_NAME 就可能包含这段字符串,Shell 随后会尝试把反引号中的内容当作子 Shell 执行。这一次没有造成损害,但这种行为本身非常危险。

用户 prompt 中的 Unicode 同样不稳。大多数情况下,UTF-8 经过 bash -lc 能正常往返;但特定 CJK 码点遇上某些 locale 设置时,会在没有任何提示的情况下丢失字节。

2.1.139 增加了 exec 形式的命令。把 args 作为字符串数组传入后,运行时会直接启动命令,中间不再经过 Shell:

JSON
{
  "hooks": {
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }]
  }
}

调用 execve 前,运行时会先从自身环境中解析 $CLAUDE_* 变量。没有 Shell,没有 Shell 插值,也没有引号处理。user's request completed 会原封不动地作为单个 argv[5] 参数传进我的 Node 脚本。

对于每次工具调用都会执行的 PreToolUse hook,这一点非常重要。

什么时候还该保留 Shell 形式?只要命令依赖管道、重定向或 glob 展开,就仍然需要它。同一个 hook 条目中的 args:[] 与 command:"" 互斥,因此若要执行 node x.js | jq | tee log,就应继续使用 command:"",并承担转义成本。对 90% 的 hooks 来说,exec 形式才是正确选择。

continueOnBlock:让 PostToolUse 拒绝真正进入重试循环

这是我等得最久的一项修复。我有一个 PostToolUse hook,专门校验所有会写入磁盘的工具调用输出:只要 Claude 写了 TypeScript 文件,hook 就运行 tsc --noEmit;一旦发现类型错误,便拒绝这次操作。

2.1.139 之前,这套拒绝流程是断的:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "command": "node hooks/validate-ts.mjs"
    }]
  }
}

validate-ts.mjs 返回 { "decision": "block", "reason": "tsc failed: ..." } 后,运行时会直接终止当前轮次,Claude 根本看不到 reason。用户只能收到一句费解的 "hook blocked the operation",还得手动粘贴错误再发一次 prompt。这个问题连续毁掉 3 次真实会话后,我停用了该 hook。

2.1.139 增加了 hook 级配置 continueOnBlock:

JSON
{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }]
  }
}

现在,block 决策会把 reason 字符串作为工具结果错误送回 Claude 的上下文。Claude 看到 tsc failed: src/api.ts(14,3): error TS2322: Type 'string' is not assignable to type 'number' 后,会在下一轮自行修正。整个循环发生在内部,用户不会看到任何中间过程。

maxAttempts 是硬上限。缺少它时,非确定性校验——例如依赖一个间歇性故障的远程 API——可能因无限重试不断消耗上下文。我的设置是 3 次;连续失败 3 次后,hook 会升级为硬阻断并通知用户。

反例也很明确:不要为决策依赖现实时间状态的 hook 启用 continueOnBlock。如果某个 hook 会在部署窗口拒绝写入,而 Claude 重试时部署仍未结束,它就会一直循环。解决办法是通过 $CLAUDE_EFFORT 设置门槛,或在 hook 脚本中加入尝试次数计数器。

两个额外亮点:$CLAUDE_EFFORT 与 CLAUDE_PROJECT_DIR

这轮更新还悄悄加入了 2 个环境变量,而且都很实用。

2.1.133 把 $CLAUDE_EFFORT 注入 hook 环境。它的取值为 low、medium、high、xhigh,对应 Claude 当前轮次的 effort level。这样一来,hook 无需解析 prompt,就能根据 effort 分支处理:

JavaScript
const effort = process.env.CLAUDE_EFFORT;

if (effort === "low" || effort === "medium") {
  // skip expensive tsc check on quick edits
  process.stdout.write(JSON.stringify({ decision: "allow" }));
  process.exit(0);
}

// run full tsc --noEmit on high/xhigh

low effort 时跳过 tsc,每次快速编辑大约能省下 800ms。到了 high 的规划轮次,即便 Claude 一次写入大约 12 个文件,完整检查仍会执行,并揪出真正的问题。

2.1.139 还把 CLAUDE_PROJECT_DIR 加入运行时启动的 stdio MCP server 环境。此前,stdio MCP server 只能通过 process.cwd() 推断工作区根目录;用户若从子目录启动 Claude Code,路径解析就会出错。现在,任何 MCP server 都可以读取 process.env.CLAUDE_PROJECT_DIR,正确解析相对于工作区的路径。

如果你在维护 MCP server,应把 manifest 的路径解析改为优先使用 CLAUDE_PROJECT_DIR,并为旧版客户端保留 cwd() fallback。只改两行,就能彻底消灭这一类问题。

我正在使用的完整 settings.json

下面是 omidsaffari-admin 的生产配置,仅做了少量脱敏。其中 6 个 DO 和 1 个 Workflow 都依赖 hook 输出完成桌面提醒与 CI 门控。

JSON
{
  "model": "claude-sonnet-4-7-20260501",
  "permissions": {
    "edit": "ask"
  },
  "hooks": {
    "SessionStart": [{
      "args": ["node", "hooks/session-start.mjs"]
    }],
    "PreToolUse": [{
      "matcher": "Bash",
      "args": ["node", "hooks/gate-bash.mjs"]
    }],
    "PostToolUse": [{
      "matcher": "Write|Edit",
      "args": ["node", "hooks/validate-ts.mjs"],
      "continueOnBlock": true,
      "maxAttempts": 3
    }],
    "Notification": [{
      "args": ["node", "hooks/notify.mjs"]
    }],
    "Stop": [{
      "args": [
        "node",
        "scripts/post-stop.js",
        "--session", "$CLAUDE_SESSION_ID",
        "--reason", "$CLAUDE_STOP_REASON"
      ]
    }],
    "PreCompact": [{
      "args": ["node", "hooks/notify.mjs"]
    }]
  }
}
JSON
{
  "devDependencies": {
    "@anthropic-ai/claude-code": "2.1.141"
  }
}

安装并验证:

Bash
pnpm add -D @anthropic-ai/claude-code@2.1.141
claude --version    # expect: 2.1.141
claude config doctor    # expect: 0 hook warnings

几个重点:continueOnBlock 与 maxAttempts 是 2.1.139 引入的重试循环组合。所有不需要 Shell 特性的 hook 都采用 args:[],而这份配置中的 hook 恰好全都如此。Notification 与 PreCompact 共用 notify.mjs,因为两者都需要通过相同的 terminalSequence 输出触发桌面提醒。

Claude Code hooks 升级清单,以及何时不该升级

升级前先为当前 settings.json 做快照。记下哪些 hooks 使用 command:""、哪些已经使用 args:[],再列出所有已接入的事件类型。

每次只迁移一种事件类型,并在每次迁移后运行 24 小时。留意 claude config doctor 是否出现 hook 警告,同时用 grep 在日志中搜索 decision 和 reason,确认运行时实际收到的内容符合预期。

我会按这个顺序进行:

  1. 将 package 升级到 2.1.141。
  2. 先把一个 command:"" hook 改为 args:[],确认它能正常触发。
  3. 为 Notification hook 添加 terminalSequence,从后台 tmux 窗格验证响铃。
  4. 为最棘手的 PostToolUse hook 添加 continueOnBlock,观察一次真实会话,确认 Claude 能看到 reason 并自行修正。
  5. 分批把其余 hooks 迁移到 args:[]。

如果当前使用的是由上层 SDK 锁定版本的托管安装,且该 SDK 尚未验证 2.1.141,就先不要升级;依赖 2.1.141 已弃用 hook 字段的项目同样如此。截至本文撰写时,5 月这轮更新没有对现有字段造成 breaking change,但向团队推广前仍应锁定版本,并在分支中完成测试。

如果想查看我在 6 个生产 agent 上采用的完整版本锁定、hook 策略与项目脚手架方案,可以阅读 Claude Code + Codex 配置清单。其中不仅包含同一套 settings.json 模式,也涵盖由 hooks 负责门控的 agent 侧脚手架(Workflows、DOs、Vectorize bindings)。

如果还想了解如何在远程 sandbox 中运行这些 agents,可以阅读配套文章 Cursor Cloud Agent 运行环境与 Cloudflare Workers 对比。至于这份 settings.json 背后的生产技术栈,可参阅 Cloudflare 100x engineer 实战解析。

terminalSequence 在 tmux 中有效吗?

有效,但有一个前提。运行时会把序列写入控制终端设备;只要 tmux.conf 中配置了 set -g allow-passthrough on,或该序列只是普通 BEL(),tmux 就会将其转发到外层终端。BEL 始终可以透传,OSC 序列则必须启用 passthrough。

同一份 hook 配置能同时使用 args:[] 和 command:'' 吗?

不能。同一个 hook 条目中,两者互斥。直接启动的命令应选 exec 形式(args:[]);需要管道、重定向或 glob 展开的命令则应选 Shell 形式(command:"")。如果确实需要混用,可以编写一个采用 exec 形式启动的 wrapper 脚本,把 Shell 功能封装在脚本内部。

如果 Claude 反复触发同一项校验,continueOnBlock 会无限循环吗?

设置 maxAttempts 后不会。运行时会把重试次数限制在该值以内,达到上限后升级为硬阻断。如果没有 maxAttempts,非确定性 validator 的确可能不断消耗上下文,因此务必设置。类型检查类校验默认设为 3 次比较合理;任何依赖远程状态的校验则应设为 1 次。

$CLAUDE_EFFORT 在所有 hook 事件中都可用吗?

是的。从 2.1.133 开始,运行时会把它注入所启动的每一个 hook 环境。其值反映当前轮次的 effort level,因此 SessionStart 读取到的是用户启动时的 effort,PostToolUse 读取到的则是工具触发时生效的值。

降级到 2.1.138 会失去哪些功能?

terminalSequence、args:[]、continueOnBlock 和 CLAUDE_PROJECT_DIR 都会悄无声息地停止工作。运行时会忽略无法识别的 JSON 字段,并退回 command:"" 解析。hooks 仍会触发,但新行为不会生效。依赖降级方案前,务必先在分支中测试。

发布日期
分类
Build
Airtable 替代品怎么选?NocoDB、Baserow 等工具的价格与适用场景

Airtable 替代品怎么选?NocoDB、Baserow 等工具的价格与适用场景

想换掉 Airtable,先分清是席位太贵、容量不够,还是缺少客户门户。本文对比 NocoDB、Baserow、SeaTable、Grist、Smartsheet、Notion 和 Softr,拆解价格、免费额度、自托管、API、自动化与权限差异,帮你按真实工作流选型,并在迁移前验证关联数据、集成和恢复方案。2026年11月10日Build
2026 年 proxy 代理服务推荐:Bright Data、Oxylabs、Decodo 等 7 家怎么选

2026 年 proxy 代理服务推荐:Bright Data、Oxylabs、Decodo 等 7 家怎么选

proxy 代理服务该怎么选?对比 Bright Data、Oxylabs、Decodo、Webshare、ThorData、IPRoyal 与 NetNut,梳理住宅、数据中心、ISP 和移动代理的价格、试用、KYC 与会话控制,分清原始网络、托管解封和抓取 API 的职责,按有效记录成本和实际任务需求筛选。2026年11月10日Build
NVIDIA OpenShell:为 AI 智能体划定可执行的权限边界

NVIDIA OpenShell:为 AI 智能体划定可执行的权限边界

NVIDIA OpenShell 如何限制编程智能体的文件、网络和凭据访问?本文梳理受支持的平台、CLI 安装与 OpenCode 启动流程,解释哪些策略可实时更新、哪些变更需要重建沙箱,并给出可审查的编程策略模板、权限测试方法和团队落地场景,帮助平台团队在开放真实项目与服务前看清实际授予的权限。2026年11月9日Build
CodeRabbit 价格详解:席位费用、用量限制与附加收费

CodeRabbit 价格详解:席位费用、用量限制与附加收费

CodeRabbit 价格怎么算?本文梳理 Essentials、Team、Advanced 与 Enterprise 的席位收费、年付折扣、PR 审查额度及关联仓库限制,并列出不同团队规模的预算算例。了解免费版和开源方案的适用范围,分清超限文件审查、Agent 分钟数与安全扫描费用,再按实际工作流和安全需求选择套餐。2026年11月9日Build
2026 企业杀毒软件怎么选:六款小企业方案、价格与响应服务对比

2026 企业杀毒软件怎么选:六款小企业方案、价格与响应服务对比

企业杀毒软件怎么选,关键在于谁来管理设备、谁来处理告警。本文对比 Bitdefender、Microsoft Defender、ESET、Sophos、CrowdStrike 与 Malwarebytes,梳理美元价格、设备额度、服务器授权和托管响应范围,帮助小企业按现有订阅、设备类型与管理能力确定候选方案。2026年11月8日Build
Paperclip AI 评测:多智能体协作怎么管,部署前要看什么

Paperclip AI 评测:多智能体协作怎么管,部署前要看什么

Paperclip AI 能把分散的 AI 智能体组织成可追踪的工作流,但安装管理面板并不等于完成权限与验收配置。本文梳理官方安装方式、运行时与模型支持、任务交接、预算策略和审批边界,并对比 OpenClaw 与 Hermes Agent,帮你判断是否值得花一个周末试用,以及上线前应检查什么。2026年11月7日Build
Supabase 是什么?看懂 AI 应用背后的数据库、权限与后端

Supabase 是什么?看懂 AI 应用背后的数据库、权限与后端

Supabase 是什么,为什么 AI 应用构建工具常用它做后端?本文从 Postgres 数据库、登录认证、文件存储、实时更新与向量搜索讲起,梳理 Lovable、Bolt、v0 和 MCP 的接入方式,解释 RLS 权限、项目控制权与迁移边界,帮你在接收真实客户数据前,明确需要验证的隔离、恢复和交接事项。2026年11月7日Build
AI IDE 怎么选?2026 年 Cursor、VS Code、Zed 等 6 款编辑器对比

AI IDE 怎么选?2026 年 Cursor、VS Code、Zed 等 6 款编辑器对比

AI IDE 怎么选?比较 Cursor、VS Code 搭配 GitHub Copilot、Windsurf(现为 Devin Desktop)、Zed、Kiro 与 JetBrains AI,梳理价格、免费方案、智能体模式、模型接入、BYOK、本地推理、隐私边界与团队管控,帮你按工作流判断是否值得迁移。2026年11月6日Build
订阅通讯

每周日,一封信。写运转中的系统,不写热评。

每周一期。无垃圾邮件。随时退订。