哪个版本发布了它——以及更新说明怎么说
Claude Code:剖析一个反功能(Misfeature)
- 时间线
- 反功能
- 修复
- 哪个版本发布了它——以及更新说明怎么说
- 文档后来才跟上
- 那么提交记录在哪?
- 为什么要加这个功能?(证据,非确认)
- 你能对比出差异吗?我们来实际对比一下
- 找出这个差异要花多大代价
- 这不仅仅是浪费代币的问题
- 关闭自动更新
- 问题:这也会停止更新你的插件
- 副作用
- 总结
- 相关文章


"Mechanical egg timer" by Hustvedt is licensed under CC BY-SA 3.0. Padded to a wider frame; this adaptation is likewise licensed CC BY-SA 3.0.
2026年加拿大国庆日(7月1日),Anthropic 向 Claude Code 用户推送了一个令人意外的“彩蛋”:2.1.198 版本包含一个效率绕过机制,允许 agent 在未获得人工指令时自行继续执行。本质上,Claude Code 提问后会给你 60 秒的计时窗口。如果你错过了这个窗口,Claude Code 会“贴心地”自行判断最佳做法,然后继续运行。效果如下:
● Claude 提问:
⎿ …
● 60 秒后无回应——未收到回答,继续执行
● 用户离开了。我将按最佳判断继续。我的计划:
注:以上内容直接引自我的一次 Claude 会话记录,问题部分已做删减。
如果你觉得这个行为很意外,你不是一个人。我们来看看可能引发的问题:
- 做三明治的时候,你是不是得把笔记本电脑也带进厨房?如果在这 60 秒内你离开了,会发生什么?
- 你同时在运行多少个 agent?你能同时观察它们全部吗?如果两个或更多 agent 在同一个 60 秒窗口内向你提问怎么办?
哪个版本发布了它——以及发布说明里写了什么
原文:
- 如果代理做出了错误的选择呢?期间又烧掉了多少 Token?
- 如果你用代理来做部署呢?(我知道,但万一呢)
你在发布这个功能时,可能会考虑这些合理的问题,也许还会在变更日志里记下你的理由。但如果变更日志里压根没提这些新默认值呢?那是不是更让人意外?(剧透:事实就是如此!)
这个故事(勉强)有个好结局。“快速行动,打破常规”并不意味着不能“快速行动,修复问题”。几天之内修复版本就发布了,但用户对这个产品的信任还剩多少呢?
我们学到了一些东西:
- Claude Code 中的惊喜功能,理论上(和实际上)Anthropic 每天都能发布一个
- 并非所有功能都会出现在变更日志中
- 不应该成为默认设置的东西,可能没有文档写明怎么关闭
- Claude Code 的自动更新功能,感觉比我们早期猜测的更像是“YOLO 模式”
还有几件事我不确定我们是否学到了:
- 人类在这其中扮演什么角色?
- 这个功能是人类想出来的吗?
- 这个功能是人类编写的(还是让代理编写的)?
- 这个功能有人审核过吗?
- 这个功能有人签字批准吗?
- 这个功能有人合并吗?
- 有人选择不记录这个功能或将其加入变更日志吗?
- 发布时,有人类发布经理对比了本版本与上一版本的差异,并在它出门前盖章批准吗?
就我个人而言,我很难相信在所有这些步骤中,会没有一个人类问一句“这是个好主意吗?”。如果你告诉我,其实是 Claude Code 自己构建了功能、发布了它、给它签了字,然后觉得不值得写文档,那反而更让我相信。但我就是不知道。也许是这两者的某种组合。也许很多地方都出了问题,但我觉得很清楚:这种事情本不该发生。说这话的人,自己就经历过至少一次绩效考核,经理说:“嗯,你确实把一个严重的 Bug 放到了生产环境里。”
哪个版本引入了这个问题——以及版本说明写了什么
我有点好奇这事是怎么发生的,以及公开记录里有没有什么复盘分析。于是,我让 Claude Code 自己调查自己。值得肯定的是,Claude 似乎没有设置什么过滤器来阻止它对这段代码进行自我反思。所以,本着完全公开的原则,下面内容主要是 Claude 自己写的,你就姑且一看吧——如果你要依赖其中的关键假设,最好还是单独复现验证一下。
Claude 的调查从这里开始。
时间线
- 2026-06-29 — 发布 2.1.196;报告人说的“最后一个正常工作的版本(我猜)”
- 2026-06-30 — 发布 2.1.197;变更日志只有一行:Sonnet 5 上线
- 2026-07-01 — 发布 2.1.198 —— 报告人认定是回归(regression)开始的那个版本。没有公开的提交记录能说明这个改动具体是什么;该版本唯一公开的痕迹是机器人提交的那条版本说明(commit 75709ea),它只修改了
CHANGELOG.md和feed.xml - 2026-07-02 02:54 UTC — Aleksey Nogin 提交了 issue #73125
- 2026-07-02 03:45 UTC — 有评论者透露了逃生舱:
CLAUDE_AFK_TIMEOUT_MS。这个信息是在问题串里用户之间互相传的,没有任何版本说明指向它 - 2026-07-02 — 发布 2.1.199,带着 24 条变更记录,而此时 issue 还是打开状态。仍然没有提到这个问题
- 2026-07-03 — 发布 2.1.200,把行为改了回来;同样,唯一的公开痕迹只有那条版本说明的提交(1322e9b)
- 2026-07-04 18:04 UTC — issue 关闭
- 问题的反响/规模:384 👍,143 条评论 —— 不是个小众抱怨
- 报告人的环境:2.1.198,“最后一个正常工作的版本 2.1.196(我猜)”,Opus,AWS Bedrock,VS Code 终端
git clone https://github.com/anthropics/claude-code.git
cd claude-code
# 修复是什么时候合入的?对应哪个版本?
git log -1 --format='%h %ai' -S'no longer auto-continue by default' -- CHANGELOG.md
# 1322e9ba 2026-07-03 16:52:26 +0000
# 携带 bug 的那个版本是什么时候发布的?
git log -1 --format='%h %ai' -S'## 2.1.198' -- CHANGELOG.md
# 75709eac 2026-07-01 20:45:29 +0000
这个不好用的功能
AskUserQuestion是 Claude Code 用来在任务中间停下来向人类提问的工具- 新行为:在 60 秒不操作 之后,这个工具会自动返回一个“继续执行”的结果,而不再阻塞等待用户回答
哪个版本发布了它——以及发布说明怎么说
- 返回给模型的消息——这是模板,默认以60秒超时渲染:
// v2.1.198,原样。`Thl` 是压缩器的命名;文字部分来自二进制文件本身。
// 注意 "60s" 是动态插入的,并非文件中的字面量:
function Thl(e){return `No response after ${Math.round(e/1000)}s — the user may be
away from keyboard. Proceed using your best judgment based on the context so far;
you can re-ask this question later if it's still relevant.`}
-
“稍后重问”的逃生口是死循环:重新提问同样会遇到这个超时。Aleksey Nogin 在提交 issue 几分钟内就指出了这一点
-
转录行有两种变体——二进制文件会根据你是否开始作答来选择使用哪一种:
// v2.1.198,原样。`a` 是压缩器对"存在一些回答"的命名
// (`s=Object.entries(r)` 遍历回答,`a=s.length>0`);两个字符串值
// 都来自二进制文件本身。
let d=a?"continued with the answers selected so far":"continued without an answer"
-
因此,一个回答了一半的对话不会丢弃已输入的部分——它会直接提交。如果你只答了三个问题中的第一个然后走开,超时触发后会提交你已答的那个,再加上模型对另外两个问题的任意回答
-
这两个字符串在 2.1.197 中不存在,在 2.1.198 中首次出现
-
公平地说:它在屏幕上并非静默。对话框会显示实时倒计时,按任意键可重置计时器。这段文本是在运行时拼接的,而不是存储为一个字符串,因此以下是渲染后的形式,无法通过直接 grep 找到:
// v2.1.198,原样——各组成部分。`s` 是剩余秒数。
children:["auto-continue in ",s,"s \xB7 any key to stay"]
// 渲染后为:auto-continue in 12s · any key to stay
-
实际效果比看起来要温和一些。倒计时只有盯着屏幕的人才能看到,而这项功能的前提恰恰是你没在看屏幕:它的内部名称是
AFK(走开状态);消息提示“用户可能已离开键盘”。同时运行多个 agent 时,“盯着屏幕”并不局限于某一处——你可能需要的倒计时正在另一个标签页里跳动。 -
倒计时来得有点晚。阈值默认是20秒(通过
CLAUDE_AFK_COUNTDOWN_MS设置),而且判断的是剩余时间,不是已过时间——所以前40秒里,这个对话框看起来就是一个普通的阻塞式弹窗。它虽然显示在屏幕上,但没有任何提示告诉你后台有个计时器在跑: -
内部名称叫 AFK(表示“用户可能已离开键盘”)
- 同时跑多个 Agent 时,“盯着屏幕”并不只有一个位置——你真正需要的那个倒计时可能在另一个标签页上
// v2.1.198 版本中这段代码被压缩了——本地变量名被打乱,
// 但属性名保留了原样,所以 `showCountdown` 和 `remainingSeconds` 这些名字还能认出来:
let u=i*1000<=n;return{remainingSeconds:i,showCountdown:u,timeoutMs:t}
// 当 n 取默认值 20000 时,等价于:
showCountdown = remainingSeconds * 1000 <= 20000
-
警告信息只在最后三分之一时间才弹出
-
它不涉及的范围:这个超时机制只对
AskUserQuestion生效。Anthropic 的 tools 参考资料里说,“权限提示(包括计划审批)在空闲时永远不会自动解决”——而且和本文其他地方引用的文档不同,这条声明是可以对照已发布的 2.1.198 版代码来验证的,而不是根据修复后写的文档页面。倒计时组件(q0m)在整个打包文件中只有一处调用点,它的定时器 hook(_Rc)也只有一个调用者——就是q0m自身。定时器存在于这个组件里,而该组件的 props 可以明确地标识它的用途:jsx(dRc,{question:V, questions:s, currentQuestionIndex:$, answers:R, questionStates:O, onAnswer:be, onSubmit:N, …})。它的超时处理器会触发tengu_ask_user_question_afk_auto_advance事件。而权限提示(“是否继续”“是否允许……”)是一个独立的组件,没有附加任何定时器。 -
但这条豁免规则只在确实弹出权限提示的情况下才保护你。2.1.198 版本内置了
bypassPermissions、acceptEdits、allowedTools、--dangerously-skip-permissions以及PreToolUse钩子。任何在部署环境中运行 Agent 的人,很可能已经把部署命令加入了白名单,或者直接关闭了权限提示——自动化本来就是这个意思。对他们来说,权限层根本不会触发,所以“定时器不碰权限提示”这条规则对他们毫无意义。
#哪个版本发布的功能——以及说明文档中写了什么
- 更窄的表述,也是真正"咬人"的地方:计时器本身不会授予权限,但它可以替你做选择。
AskUserQuestion并不是在请求权限,而是让你自己决定——"staging 还是 production?""用哪个配置?"。超时后,模型被告知"用你最好的判断"继续,而部分回答的路径会"沿着目前已选的答案"走下去。如果权限已经通过 allowlist 或 bypass 被授予,那么选择就是最后一道拦路卡。 - 工具 schema 中没有
timeout参数——模型既无法设置它,也无法控制它。这在二进制文件中得到了验证(U_f=…H.strictObject({questions:…})),并非来自 issue 讨论串。输入参数只有: - 倒计时组件(
q0m)在整个 bundle 中有且只有一个调用点,它的计时器 hook(_Rc)也只有一个调用者——即q0m自身。这个计时器只存在于一个组件中。 - 那个组件的 props 通过参数可以识别出来:
jsx(dRc,{question:V, questions:s, currentQuestionIndex:$, answers:R, questionStates:O, onAnswer:be, onSubmit:N, …})。它的超时处理函数触发的正是tengu_ask_user_question_afk_auto_advance。 - 权限提示("Do you want to proceed""Do you want to allow …")是一个独立的组件,它没有计时器。
questions, answers, annotations, metadata
- 所以说,模型声称它"没有跳过任何内容"是实话——是 harness 返回了答案,而不是模型本身。
#修复
- 2.1.200 版本将 auto-continue 的默认值改成了关闭。
- 空闲超时现在需要通过
/config主动选择加入,不再对所有用户强制启用。 - 注意 changelog 的措辞:"不再默认自动继续"。没有删除任何功能——只是翻转了默认值。
- changelog 中指向的那个
/config配置项,在修复前并不存在。在 2.1.198 版本中搜索askUserQuestionTimeout是零结果:当这个功能发布时,唯一的绕开方式是一个环境变量——而发布说明中从未提及过它。
#哪个版本引入了该功能——发行说明说了什么
-
你可以在当前版本(2.1.211,已过去11个版本)的二进制文件中确认其余部分。整个机制完整保留:设置项名为
askUserQuestionTimeout,在/config中显示为“Question auto-continue timeout”;可取值是60s、5m、10m、never——未设置时默认为never,这使它成为“主动选择加入”模式。钩子的默认超时常量仍然是60000ms;倒计时阈值为20000ms。两个环境变量仍可覆盖该设置——CLAUDE_AFK_TIMEOUT_MS和CLAUDE_AFK_COUNTDOWN_MS -
因此修复只是在开关处改了一行代码,而不是删除功能:
-
设置项名为
askUserQuestionTimeout,在/config中显示为“Question auto-continue timeout” -
可取值是
60s、5m、10m、never——未设置时默认为never,这使其成为主动选择加入模式 -
钩子的默认超时常量仍然是
60000;倒计时阈值是20000 -
两个环境变量仍可覆盖该设置——
CLAUDE_AFK_TIMEOUT_MS和CLAUDE_AFK_COUNTDOWN_MS
// v2.1.211,逐字复制——仅添加了空格。`Upf`是压缩工具为其起的名字;
// 此处的其他内容均来自二进制文件本身,包括字符串值。
function Upf(e){ switch(e){
case "60s": return 60000; case "5m": return 300000;
case "10m": return 600000; case "never": case void 0: return null; } }
// ^^^^^^^^^^^^ 未设置 => null => 禁用
-
公允地说:这是正确的修复。从一开始就应该设计为主动选择加入,而且这个能力对某些人来说确实有用。
-
让人不太舒服的解读是:那段曾为你自动回答的代码仍然在发行版本中,只隔着一个配置项,且由同一种流程控制——正是这个流程最初让它悄悄开启的。
-
修复前的临时解决方案(源自相关讨论串),供仍卡在受影响版本的人使用:
// settings.json — 通过设置一个极大的AFK窗口来禁用自动继续
"env": {
"CLAUDE_AFK_TIMEOUT_MS": "<一个巨大的数值>"
}
- 响应速度很快——从报告到撤销大约只用了两天(这点值得肯定)
##哪个版本引入了该功能——发行说明说了什么
-
发行说明发布在两个地方:官方更新日志(official changelog)和仓库中的
CHANGELOG.md(内容相同) -
下面的链接固定指向提交
1322e9b,因此它们显示的是当时发行说明的内容,而非后来编辑过的版本
哪个版本发布的——以及发布说明写了什么
-
2.1.197变更日志:一行,Claude Sonnet 5 发布。关于提问超时什么都没提。
-
2.1.198变更日志:约30条。关于 AskUserQuestion 自动继续功能什么都没提。
-
2.1.199变更日志:24条,发布时问题已经公开。依然什么都没提。
-
60秒自动继续功能在添加时从未在任何发布说明中宣布过。
-
AskUserQuestion 在变更日志里并不陌生——从 2.0.55 到 13 个版本中出现了 15 次。这是 Anthropic 会常规记录变更的工具:
git show 1322e9b:CHANGELOG.md | grep -c 'AskUserQuestion'
# 15
git show 1322e9b:CHANGELOG.md | awk '/^## /{v=$2} /AskUserQuestion/{print v}' | sort -u | tr '\n' ' '
# 2.0.55 2.1.136 2.1.141 2.1.144 2.1.147 2.1.181 2.1.200 2.1.47 2.1.69 2.1.70 2.1.83 2.1.85 2.1.9
-
所以,空白期才是关键。在 2.1.181 和 2.1.200 之间——也就是包含改动的那个窗口——它一次都没出现。行为变了两次:先开启,后关闭,而发布说明只记录了第二次。
-
唯一一条提到自动继续功能的变更日志是删除它的那条,在 2.1.200 版本:
## 2.1.200
- 修改了 `AskUserQuestion` 对话框的默认行为:不再自动继续;
可通过 `/config` 选择进入空闲超时模式。
- 控制该行为的环境变量
CLAUDE_AFK_TIMEOUT_MS在变更日志和 README 中从未出现过。
#文档后来补上了
- 现在,两个环境变量都已经记录在环境变量参考文档中。该条目对这段历史直言不讳:
CLAUDE_AFK_TIMEOUT_MS — 在无人回答的 AskUserQuestion 对话框自动继续之前,
允许的空闲时间(毫秒)。默认关闭自动继续;通过 askUserQuestionTimeout 设置
选择启用。[…] 在 2.1.198 和 2.1.199 版本中,自动继续默认开启,超时时间为
60000 毫秒(60 秒)。
- 但这段文字不可能是 7 月 1 日就有的,而二进制文件本身也说明了这一点:它指向的是
askUserQuestionTimeout作为启用选项,而这个设置在 2.1.198 二进制中出现了零次。它还把 2.1.198 和 2.1.199 描述为过去式,就像一个已结束的时间段。
#哪个版本发布了它——发布说明又说了什么
- 而 Wayback Machine 直接给出了定论。这套文档没有公开的仓库,所以我原本以为它的历史无从查证。其实不然——这个页面被反复存档,正好覆盖了那个时间段:
# 环境变量参考页面的每一次存档抓取,时间范围 2026年6月23日 - 7月11日
curl -s "https://web.archive.org/cdx/search/cdx?url=code.claude.com/docs/en/env-vars\
&output=json&filter=statuscode:200&fl=timestamp&from=20260601&to=20260718"
# 依次获取每个存档,统计功能提及次数
# (DISABLE_AUTOUPDATER 作为对照:每次存档都应该匹配到)
for ts in 20260623083334 20260701121132 20260701213540 20260705135805; do
curl -s "https://web.archive.org/web/${ts}id_/https://code.claude.com/docs/en/env-vars" \
| gunzip -c | grep -c -i 'CLAUDE_AFK'
done
-
版本 2.1.198 于
2026-07-01T16:50:16Z发布到 npm。21:35 的存档比它晚了 4 小时 45 分钟——但页面上没有提到该功能的任何名称:没有afk,没有auto-continue,没有AskUserQuestion,也没有COUNTDOWN。(idle出现了两次,但都不相关:API_FORCE_IDLE_TIMEOUT和 MCP 工具超时。)对照项每次存档都能匹配到,所以这是真的缺失,而不是 grep 命令出了毛病。 -
于是两个问题合并成了一个答案。在该功能发布当天,它既不在发布说明里,也不在文档里。用户没有任何渠道能得知这一消息。
-
文档出现在 7 月 1 日 21:35 到 7 月 5 日 13:58 之间——这个时间窗口包含了 2.1.200 的回滚(
2026-07-03T04:33:49Z)。而后来出现的条目将 auto-continue 描述为 “默认关闭”,这只有在修复之后才成立。文档从未描述过 7 月 1 日和 2 日实际存在的情况。它们是在回滚时出现的,并且记录了回滚后的行为。 -
换句好听的话说,文档后来补上了。但它们实际做的是跳过了“它默认开启”的那一部分。
#那么提交在哪里?
-
下一步很自然:打开引入该功能的那个提交,读一读它的理由。
-
但没有这样一个提交。不是“很难找到”——它根本不存在于公开的地方。
-
回滚提交也同样不存在。2.1.200 中的回滚也没有公开提交。
哪个版本发布了它——以及发布说明写了什么
两个发布在 Git 里留下的唯一痕迹是两条自动生成的变更日志提交,标题都是 chore: Update CHANGELOG.md and feed.xml——这些是关于发布的说明,而不是发布本身:
- 75709ea——发布 2.1.198 的说明(那个功能随此版本发布,但说明里只字未提)
- 1322e9b——发布 2.1.200 的说明(回滚)
那好,那就比对这两个版本之间的源码差异吧。但是——根本没有源码可对比。
anthropics/claude-code 仓库不是产品本身。它只是变更日志、文档、插件示例、几个示例基础设施配置,以及负责处理 Issue 跟踪器的机器人:
git ls-files | wc -l # 216 个跟踪文件
git ls-files '*.md' | wc -l # 其中 104 个是 markdown
git ls-files | cut -d/ -f1 | sort -u | grep -v '^\.'
# CHANGELOG.md demo.gif examples feed.xml LICENSE.md
# plugins README.md Script scripts SECURITY.md
里面任何可执行文件要么是示例,要么是维护脚本。plugins/ 存放的是插件示例,examples/ 里有一个 GCP 网关 Terraform 配置和一个 MDM 配置文件,scripts/ 是八个 Issue 跟踪器自动化文件(auto-close-duplicates.ts, sweep.ts, gh.sh)。没有任何东西真正推送给你。
不过这个仓库确实打了发布标签,所以这些标签至少看上去能拿来对比。但实际上,它们并不可比——至少在关键意义上不行:
git diff --stat v2.1.197..v2.1.198
# CHANGELOG.md | 35 +++++++++++++++++++++++++++
# feed.xml | 77 +++++++++++++++++---------------------------------
# 2 files changed, 74 insertions(+), 38 deletions(-)
-
feed.xml是把更新日志(changelog)转成了 RSS 格式,所以上面的 diff 其实展示了同一份更新日志两次。在连续十个版本(2.1.196→2.1.206)中,每个标签之间的 diff 只动了这两个文件,其他文件一概没碰。 -
关键在于:发布标签之间的差异(diff)本身就是发布说明。这些是“发布说明标签”(release-note tags),而不是“源码标签”(source tags)——你没法 checkout 出任何一份代码版本。
-
于是,“看发布说明”这条路走不通(因为说明里的变化是无声的),“对仓库做 diff”也行不通(因为没有源码),“对标签做 diff”同样没用(因为标签本身就是说明)。三条死路,同一个原因:Anthropic 发布到 git 上的内容,跟他们实际交付给你的东西根本不是一回事。
-
原始源码从未公开发布;所有行为只编译在二进制的可执行文件里。
-
因此,这个功能曾经存在过的全部公开证据,只有这些:
- 它打印到用户终端的那条消息
- 一个环境变量(
CLAUDE_AFK_TIMEOUT_MS),用户是通过互相打听才知道它的存在 - 没有任何发布说明提过它
- 三天后,更新日志里出现了一行,宣布将其移除
-
这个功能的引入,在发布说明和 git 里都没留下任何痕迹。而它的删除,反而是发布说明第一次提及它。
-
需要把两个容易混为一谈的主张分开来看:
- 没有公开的源代码仓库——确实,这是治理问题
- 无法知道究竟交付了什么——错误,如下文所示,这一点其实至关重要
-
如果着急,可以直接跳到后面:交付的二进制文件能解决 git 无法回答的问题。这个功能在 2.1.197 中确实不存在,而在 2.1.198 中确实存在,你花大约五分钟就能自己验证。
feed.xml 是把更新日志转成了 RSS 格式,所以上面的 diff 其实展示了同一份更新日志两次。在连续十个版本(2.1.196→2.1.206)中,每个标签之间的 diff 只动了这两个文件,其他文件一概没碰。
关键在于:发布标签之间的差异(diff)本身就是发布说明。这些是“发布说明标签”,而不是“源码标签”——你没法 checkout 出任何一份代码版本。
哪个版本带了它——发布说明又说了什么
所以,“去看发布说明”这条路走不通(静默变更);“diff 仓库”这条路也走不通(没有源码);“diff 标签”同样走不通(标签本身就是发布说明)。三条死胡同,同一个原因:Anthropic 放到 git 上的东西,跟它实际发给你的是两码事。
作者写的源码从未公开过;行为只存在于编译后的二进制文件里。
因此,这个功能曾经存在过的公开证据,全部加起来就是:
- 它在用户终端上打印的那条提示信息
- 一个环境变量(
CLAUDE_AFK_TIMEOUT_MS),是用户互相问出来的,没有发布说明提过它 - 三天后的一条变更日志,宣布删除它
这个功能的引入,在发布说明和 git 里都没留下痕迹。而它的删除,反而是发布说明第一次提到它。
这里有必要把两件容易混为一谈的事拆开:
- 不存在公开的源代码仓库——这是真的,也是治理层面的问题
- 没办法看到实际发布了什么——这是假的,而且这一点特别重要(后面会讲)
想跳过也行:发布的二进制文件能回答 git 回答不了的问题。这个功能可以证实——在 2.1.197 里没有,在 2.1.198 里就有,而且你自己花五分钟就能验证。
为什么加入这个功能?(证据而非确认)
- 官方从未发布过任何理由(没有设计文档、没有变更日志行、没有 PR——见下文)
- 旁证来自命名和提示文字:
- 内部名称为
AFK——"away from keyboard"(离开键盘),对应CLAUDE_AFK_TIMEOUT_MS - 提示信息假设用户不在:
"the user may be away from keyboard" - 合理的适用场景:无人值守/多并行 agent 任务,否则会因用户不在而永远阻塞
- 讨论帖里的用户描述的就是该功能破坏的工作流:几十个 agent,有些挂在那里好几天,本来就等着用户(早期体验版)
- 矛盾之处:这个本来是为了让用户不在时不阻塞的设计,同时也允许 agent 替你做那些你本来明确留给自己做的决策
- 最强证据在二进制文件里,而且就在 2.1.198 本身。这是两件不同的事,值得分开来看——
哪个版本发布的功能——以及发布说明写了什么
-
工具自身 schema 上的一个字段——它只留在本地。这个字段会跟随工具结果一起返回,告诉模型答案是自动解析出来的,而不是手动选择的,并决定由哪个组件渲染对话记录行:
-
内部名称为
AFK——"away from keyboard"(离开键盘)的缩写(CLAUDE_AFK_TIMEOUT_MS) - 消息假设用户已不在:"the user may be away from keyboard"(用户可能已离开键盘)
- 合理的使用场景:无人值守 / 大量并行 agent 运行——否则这些运行会因用户不在而永远阻塞住
javascript
// 来自 v2.1.198,逐字复制。`H` 是压缩器给 schema 库(Zod)起的名称;
// 字段名和描述文本都是二进制文件自带的。
afkTimeoutMs: H.number().int().positive().optional().describe("Set when the dialog
auto-resolved after this many milliseconds of idle (user away from keyboard).
Absent on every human-resolved path.")
- 一个分析事件——它会离开你的机器。在对话框自动推进的那一刻触发:
javascript
// 来自 v2.1.198,逐字复制。压缩后的名称(q, ld, It, R, I, s)是压缩器起的;
// 事件名和所有属性键都是二进制文件自带的。
q("tengu_ask_user_question_afk_auto_advance",{...i&&{source_hash:ld(i)},
timeoutMs:It, questionCount:s.length,
hadPartialAnswers:Object.keys(R).length>0, isInPlanMode:I})
tengu_*是整个二进制文件中 Claude Code 分析事件的命名规范。不会发送问题文本;source_hash是一个哈希值,其余都是计数器。- 这个事件在 2.1.197 版本中不存在。它与该功能在同一版本中一起出现。
- 从它的 payload 可以读出它的用途:一个统计对话框在没有人工参与的情况下被解析了多少次、有多少问题处于挂起状态、是发生在计划中途还是其他情况、以及你是否已经给出了部分回答的计数工具。
- 最后一项是
hadPartialAnswers。半回答的情况并非某个没人预料到的疏忽——它有专门的代码路径,而且这个路径与其他情况是分开计数的。
因此:这个功能是有意构建的,而不是偶然撞上的。行为、倒计时、schema 字段和分析事件全都在同一个版本中出现——这是一个附带测量机制的功能,而不是一个随手设置的默认值。
-
注意这个证据没有说明什么。它完全没有提到“谁”,甚至是否有人参与其中。本文开头提出的问题依然悬而未决;这个二进制文件只能证明这项工作是有连贯性的、经过深思熟虑的,但无法说明是谁或者什么做了这件事。
-
同一个版本里,它在你面前打印了倒计时:
auto-continue in {n}s · any key to stay -
所以这个功能被设计出来、加了埋点、还给了UI——却始终没有出现在发布说明里。这正是本文要讨论的缺口:这件事并不是小到不值得提。有人为它搭了一套测量工具。
-
所有这些信息都没告诉我们是谁批准了它,也没说是否有人权衡过失败模式和收益。仍然是推测,仍然没有公开的决策依据。
#你能对它做二进制差异分析吗?
-
以上说的所有都指向不能。但当你去看看实际安装的产物,答案就变了。
-
它是一个大约250MB的本地可执行文件——而且关键在于,没有被 strip:
file ~/.local/share/claude/versions/2.1.211
# ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
# interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=..., not stripped
- 这是一个用 Bun 编译的二进制文件。Bun 的单文件可执行文件会在运行时末尾追加一个模块图,前面有一个魔术标记——它就在那里:
grep -abo -- '---- Bun! ----' ~/.local/share/claude/versions/2.1.211 | tail -1
# 261973153 <-- 距离262MB文件末尾约50KB的位置
- Bun 的编译方式是把 JS 包嵌入到可执行文件内部,所以推送的 JavaScript 就静静地躺在文件里。用
strings就能读到:
strings -n 3 ~/.local/share/claude/versions/2.1.211 | grep -c 'CLAUDE_AFK_TIMEOUT_MS'
# 4
-
上面“修复”一节中的每一个断言,都是从这个二进制里直接读出来的——设置名称、
60s/5m/10m/never这些值、分析事件、倒计时环境变量,全都有。 -
安装程序会把最近的版本保留在磁盘上(
ls ~/.local/share/claude/versions/),但只保留最近几个——不足以回溯到七月。 -
不过,你可以在 npm 上找到任何一个你想要的版本。关键是:
@anthropic-ai/claude-code是一个大约 152KB 的安装程序存根(7个文件,dist.unpackedSize为155,204)——里面不包含产品代码。真正的二进制文件位于各平台特定的包中:
哪个版本包含了它——以及发布说明怎么说
npm view @anthropic-ai/claude-code-linux-x64@2.1.198 dist.unpackedSize
# 248900994 <-- ~249MB,实际文件大小
- 所以:闭源,但不是黑盒。每个版本都可以下载,每个版本都可解读。这个区别很重要。
#那就实际 diff 一下看看
-
别光纸上谈兵,直接动手:从 npm 拉下
2.1.197和2.1.198两个版本,diff 一下。 -
这个功能内部有个独特的名字——
AFK。在两个二进制文件里搜一下:
for v in 2.1.197 2.1.198; do
echo "=== $v ==="
for s in "away from keyboard" "CLAUDE_AFK_TIMEOUT_MS" "CLAUDE_AFK_COUNTDOWN_MS"; do
printf ' %-26s ' "$s"; strings -n 3 "b-$v/package/claude" | grep -c -- "$s"
done
done
# === 2.1.197 ===
# away from keyboard 0
# CLAUDE_AFK_TIMEOUT_MS 0
# CLAUDE_AFK_COUNTDOWN_MS 0
# === 2.1.198 ===
# away from keyboard 2
# CLAUDE_AFK_TIMEOUT_MS 3
# CLAUDE_AFK_COUNTDOWN_MS 3
-
从零到非零,恰好跨过了爆料者指出的那个版本边界。没有 commit、没有 changelog 条目、没有 PR——但产物明确地告诉了你它是什么时候落地的。
-
这就是 git 里不存在的那份记录。它一直存在于他们发给你的东西里。
-
而且它明确了这次发布到底做了什么。决定对话框是否出现计时器的那个控制门,两个版本并排对比:
// 两者逐字相同,仅增加了空白。混淆后的名称 (ke, Ie, li, OOb, Rpf,
// Di, Jyn) 是压缩器生成的;`hasExternalRacer` 和环境变量是二进制自带的。
// v2.1.198
ke = !Ie && !n.hasExternalRacer && !li()
// v2.1.211 — 三个条件相同,多了一个
OOb = !Rpf && !Xde.hasExternalRacer && !Di() && (Jyn !== null || ye.CLAUDE_AFK_TIMEOUT_MS !== void 0)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 整个修复所在
- 这里的
Jyn就是afkTimeoutMs属性——即/config设置项,通过上面的开关解析得来。这就是全部差异:在 2.1.198 中,门存在,但没有任何你可设置的东西允许它关闭。在 2.1.211 中,它先询问你许可。
哪个版本引入了它——以及更新说明说了什么
-
得说清楚,因为这种事情容易被夸大:并不是说 2.1.198 版本根本就没有任何限制开关。它是有这个开关的,而且只要对话框里没有外部玩家,它就会自动开启。问题在于,它缺少任何用户能够影响的判断条件。
-
两天的抗议之后,修复方案就是一个
&&子句。这也正是关键所在:它距离不发生问题,就差那么一个&&子句。 -
但注意那个 grep 需要什么:反馈者给了我们版本号,而且这个功能有名字可以 grep。普通周三的日常工作中,这两个条件都不成立。所以真正的问题是:在既不知道版本号、也不知道功能名的情况下,你是否能找到这个冷门问题。
-
对 2.1.197 → 2.1.198 做一次盲字符串级别 diff:
for v in 2.1.197 2.1.198; do strings -n 8 "b-$v/package/claude" | sort -u > "s-$v.txt"; done
diff s-2.1.197.txt s-2.1.198.txt | grep -c '^[<>]'
# 21903
- 一次发布就改了 21,903 个字符串。压缩器每次构建都会重命名所有标识符,所以几乎全是噪音,不是实际变化。
CLAUDE_AFK_TIMEOUT_MS就藏在里面——淹没在 21,902 行噪声之中。
得说清楚,因为这种事情容易被夸大:并不是说 2.1.198 版本根本就没有任何限制开关。它是有这个开关的,而且只要对话框里没有外部玩家,它就会自动开启。问题在于,它缺少任何用户能够影响的判断条件。
两天的抗议之后,修复方案就是一个 && 子句。这也正是关键所在:它距离不发生问题,就差那么一个 && 子句。
但注意那个 grep 需要什么:反馈者给了我们版本号,而且这个功能有名字可以 grep。普通周三的日常工作中,这两个条件都不成立。所以真正的问题是:在既不知道版本号、也不知道功能名的情况下,你是否能找到这个冷门问题。
对 2.1.197 → 2.1.198 做一次盲字符串级别 diff:
for v in 2.1.197 2.1.198; do strings -n 8 "b-$v/package/claude" | sort -u > "s-$v.txt"; done
diff s-2.1.197.txt s-2.1.198.txt | grep -c '^[<>]'
# 21903
- 21,903 个字符串变化对应一次发布。压缩器每次构建都会重命名所有标识符,所以几乎全是噪音,不是实际变化。
CLAUDE_AFK_TIMEOUT_MS就藏在里面——淹没在 21,902 行噪声之中。
这里就是 afkTimeoutMs 属性——也就是 /config 设置项,通过上面的 switch 解析得到。这就是全部差异:在 2.1.198 版本中,这个开关存在,但没有任何你可以设置的选项能够把它关掉。在 2.1.211 版本中,它会先征得你的许可。
得说清楚,因为这种事情容易被夸大:并不是说 2.1.198 版本根本就没有任何限制开关。它是有这个开关的,而且只要对话框里没有外部玩家,它就会自动开启。问题在于,它缺少任何用户能够影响的判断条件。
两天的抗议之后,修复方案就是一个 && 子句。这也正是关键所在:它距离不发生问题,就差那么一个 && 子句。
但注意那个 grep 需要什么:反馈者给了我们版本号,而且这个功能有名字可以 grep。普通周三的日常工作中,这两个条件都不成立。所以真正的问题是:在既不知道版本号、也不知道功能名的情况下,你是否能找到这个冷门问题。
哪个版本发布的——以及发布说明写了什么
-
对这个数字要留个心眼,因为它看起来比实际更确定:它是
invocation的属性,不是release的属性。strings -n 3给出81,289行;默认的-n 4给出29,910行;-n 8给出21,903行。不管用哪个参数,结果的性质都一样——几万行,绝大多数是噪音——但不要把21,903当作一个固定常量。 -
现在过滤出看起来像英文句子的内容。只保留新增的行,这些行由字母和普通标点组成,以大写字母开头,至少五个单词。这样就去掉了所有混乱的标识符、路径和代码片段:
# 仅新增字符串:出现在198版本,不在197版本中
diff s-2.1.197.txt s-2.1.198.txt | grep '^>' | sed 's/^> //' > added.txt
wc -l < added.txt
# 16255
# ...那些像英文句子的行
grep -E '^[A-Z][a-zA-Z0-9 ,.:;'"'"'-]+$' added.txt | awk 'NF>=5' > prose.txt
wc -l < prose.txt
# 156
- 156行。这就是该版本中所有新增的人类可读文本——少到可以边喝咖啡边读完。而那个特性就在其中,用大白话写着:
Before going idle the user had selected:
-
这就是当对话替你回复时,注入回对话中的那段字符串。如果你事先什么都不知道,冷读这行文字,你一定会停下来琢磨。
-
它在156行中排在第11行,听起来像是运气好,其实不然:
s-*.txt是用sort -u排序过的,所以列表是按字母顺序排列的。前面有10行以“A”开头,而它以“B”开头。位置在这里说明不了什么——关键是156行用五分钟就能读完,而不是字母表对你特别友好。
找到它的代价
-
所以 diff 是有效的。一个人在7月1日做这件事,没有任何特权信息,就能在它发布当天——也就是问题被提交之前——发现它。
-
于是就得出一个诱人的结论:“那就每个版本都做 diff 呗。”
哪个版本发布的——以及发布说明写了什么
-
这就回答了文章开头提出的问题之一:在版本发布之前,人类发布经理有没有拿着新版本和上一版做比对?
是谁把 2.1.198 发出去的,那个人拥有我完全没有的优势:源码、构建结果、差异对比、代码审查、可以找作者问。
而我只有一条curl和一个grep,花了大约五分钟。
所以结果只有两种可能:要么没人看过,要么有人看了但还是给发了。两种都是答案,哪一种都不好听。
剩下那些问题——是谁写的、谁审核的、谁签字放行的——永远无法回答,因为能记录这些信息的公开日志压根不存在。这和缺失的提交记录是同一个空洞,只是换了一顶帽子。 -
这是一个让人不安的发现,而不是令人放心的结论:
你能查出来吗?
能。事实已经证明。只需要一条curl、一个strings和一个diff。
你应该非得这么做吗?
每个版本 156 行差异,永久如此,所有自动更新的工具都要这样去查,就为了知道本来一行发布说明就能告诉你的东西。
没人会这么干。最暴露在风险中的人——那些大规模运行无人值守代理的人——恰恰最不可能凌晨两点去 grep 二进制文件。 -
这种能力只是一种权宜之计,不是解决办法。它能工作,恰恰是它难以被接受的原因。
-
是谁把 2.1.198 发出去的,那个人拥有我完全没有的优势:源码、构建结果、差异对比、代码审查、可以找作者问。
-
而我只有一条
curl和一个grep,花了大约五分钟。 -
所以结果只有两种可能:要么没人看过,要么有人看了但还是给发了。两种都是答案,哪一种都不好听。
-
剩下那些问题——是谁写的、谁审核的、谁签字放行的——永远无法回答,因为能记录这些信息的公开日志压根不存在。这和缺失的提交记录是同一个空洞,只是换了一顶帽子。
-
你能查出来吗?能。事实已经证明。只需要一条
curl、一个strings和一个diff。 -
你应该非得这么做吗?每个版本 156 行差异,永久如此,所有自动更新的工具都要这样去查,就为了知道本来一行发布说明就能告诉你的东西。
-
没人会这么干。最暴露在风险中的人——那些大规模运行无人值守代理的人——恰恰最不可能凌晨两点去 grep 二进制文件。
#为什么这不仅仅是白白消耗token
-
成本角度:一个无人值守的agent自作主张回答问题,可能在错误的路径上疯狂消耗token
-
安全角度的后果比成本更严重:
AskUserQuestion是一个显式的安全闸门——基于它构建的钩子(hooks)/规则假定它会拦截动作。把这个拦截闸门变成60秒倒计时,就会悄无声息地让那些假设失效。人们确实会在高风险场景中运行Claude Code——比如部署、基础设施、与生产环境相关的脚本 -
叠加因素:Claude Code默认自动更新。行为静默改变 + 自动更新 = 你什么都没做,闸门就在你眼皮底下变了样。这又回到了《论冷却时间和Dependabot调优》里的观点——为什么“立刻用最新版”是一种风险姿态
-
AskUserQuestion是显式安全闸门——基于它建的钩子/规则假定它拦截 -
把拦截闸门变成60秒倒计时,就悄悄推翻了那个假设
-
人们确实在真正的危险场景里用Claude Code——部署、基础设施、贴近生产环境的脚本
-
行为静默改变 + 自动更新 = 你啥都不用干,闸门就在你眼皮底下变了
-
又和《论冷却时间和Dependabot调优》那段话连上了——为什么“立刻上最新版”是一种风险姿态
#关闭自动更新
- 三个环境变量。解析器按这个顺序检查,匹配第一个就生效——注意不是你直觉里的顺序:
DISABLE_UPDATES=1—— 第一个被检查。最严格:拦截所有更新路径,包括手动执行claude updateDISABLE_AUTOUPDATER=1—— 只停下后台检查;claude update仍然能用。优先级高于autoUpdates配置项-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1—— 大锤子:相当于DISABLE_AUTOUPDATER+DISABLE_FEEDBACK_COMMAND+DISABLE_ERROR_REPORTING+DISABLE_TELEMETRY(旧的名称是DISABLE_BUG_COMMAND,仍然接受) -
它们都会把你的插件也冻结住。三个变量里只有一个明确写了这条——见下文
-
“到处都要设”这个问题有解决办法:不要从shell配置文件里导出变量——把它放在
settings.json的env块里,官方文档对这个块的描述是“每个会话都会生效的环境变量”。一个文件,每次调用都生效——包括CI、cron、systemd以及IDE拉起的终端
哪个版本引入的——以及更新日志怎么说?
DISABLE_UPDATES=1— 最先检查。最严格的设置:阻止所有更新路径,包括手动执行claude update。DISABLE_AUTOUPDATER=1— 仅停止后台检查;claude update仍然可用。优先级高于autoUpdates配置项。CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1— 大锤方案:相当于DISABLE_AUTOUPDATER+DISABLE_FEEDBACK_COMMAND+DISABLE_ERROR_REPORTING+DISABLE_TELEMETRY(DISABLE_BUG_COMMAND是老名称,仍然兼容)。
// ~/.claude/settings.json — 用户级别,对每一个 shell / IDE / CI 调用都生效
{
"env": {
"DISABLE_AUTOUPDATER": "1"
}
}
- 作用域(从最低到最高持久性):
- shell 配置文件中的 export → 仅适用于当前 shell,容易漏掉某些环境(常见陷阱)
~/.claude/settings.json→ 全局用户,单一位置- 仓库中的
.claude/settings.json→ 团队共享,纳入版本控制 managed-settings.json→ 企业策略,最高优先级,统一强制实施
macOS: /Library/Application Support/ClaudeCode/managed-settings.json
Linux/WSL: /etc/claude-code/managed-settings.json
Windows: C:\Program Files\ClaudeCode\managed-settings.json
(C:\ProgramData\ClaudeCode 是旧路径,已不再读取)
- 目前没有用于禁用自动更新的 CLI 标志;
claude update是手动命令,/doctor会报告更新渠道和安装类型。
注意:这也会阻止插件更新
- 通过上述任何一种方式关闭自动更新,插件自动更新也会随之停止。
- 这一点其实是有文档说明的——但藏在了“插件发现”页面的“配置自动更新”部分,而你关掉自动更新时通常不会在那里:
要彻底禁用 Claude Code 和所有插件的自动更新,
设置 DISABLE_AUTOUPDATER 环境变量。
要保持插件自动更新启用而仅禁用 Claude Code 自动更新,
则设置 FORCE_AUTOUPDATE_PLUGINS=1 并同时设置 DISABLE_AUTOUPDATER。
哪个版本发布了它——以及发行说明说了什么
-
哪些地方没提到它:
/setup→ 禁用自动更新,那个教你如何操作的页面,既没提插件,也没提FORCE_AUTOUPDATE_PLUGINS。settings环境变量表同样没提。你如果只跟着自己正在做的任务文档走,压根儿不会知道这个事。 -
文档始终只说了
DISABLE_AUTOUPDATER。但二进制代码里实际有四种方式冻结插件——DISABLE_UPDATES、CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC和autoUpdates: false也能达到同样效果,而这部分完全没有文档。它们统一由一扇门控制:“更新器是否因任何原因被关闭”:
// 来自 v2.1.211,原文复制。函数名已无意义,因为压缩器把它们吞掉了——
// 环境变量名和日志字符串是字符串而非标识符,所以保留了下来。
function u_e(){return h$e()!==null}
function blt(){return u_e()&&!ut(process.env.FORCE_AUTOUPDATE_PLUGINS)}
// ...然后在插件自动更新入口点:
if(blt()){w("Plugin autoupdate: skipped (auto-updater disabled)");return}
- 这段代码很难读,所以我用自己的命名重写了一遍——
h$e→disabledReason,u_e→updatesAreDisabled,blt→pluginUpdatesBlocked,ut→truthy,w→debugLog。逻辑没动;函数名是我根据意图猜的,不是原作者写的:
// 不是发布的代码——下面的函数名是我为了可读性自己起的。
// 控制流、环境变量名和日志字符串与 v2.1.211 完全一致。
function updatesAreDisabled(){ return disabledReason() !== null }
function pluginUpdatesBlocked(){
return updatesAreDisabled() && !truthy(process.env.FORCE_AUTOUPDATE_PLUGINS)
}
if (pluginUpdatesBlocked()) { debugLog("Plugin autoupdate: skipped (auto-updater disabled)"); return }
-
运行时,这次跳过几乎不可见——它只是一条 debug 日志,不是警告。无论是否有文档,你在会话中都看不到任何提示告诉你插件已经冻结了。
-
修复方法就是再加一个环境变量——那个有文档、官方支持的环境变量。它能在固定 CLI 版本的同时恢复插件更新:
// ~/.claude/settings.json — 固定 CLI 版本,保持插件更新
{
"env": {
"DISABLE_AUTOUPDATER": "1",
"FORCE_AUTOUPDATE_PLUGINS": "1"
}
}
FORCE_AUTOUPDATE_PLUGINS、DISABLE_AUTOUPDATER和DISABLE_UPDATES现在能正确解析了:解析器接受1、true、yes或on(不区分大小写),所以=1会如你所愿开启,而=0则会正确识别为关闭。CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC则不是这样。它是基于“存在性检测”的——只要值非空就会被当作开启,=0也不例外。这意味着CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=0会同时禁用自动更新并冻结你所有的插件:
// v2.1.211,原文如此。`Z3t` 是压缩器的名称;环境变量是二进制文件中的那个。
// 注意这里的 `process.env.X` —— 值从未被读取,只检查其是否存在。
function Z3t(){if(process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC)return"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC";return null}
- 注意这个模式的形状:一个以
FORCE_开头的变量,才是保持插件始终为最新状态的“正常”做法。这看起来像是随手加的一个逃生舱门。
小心边缘情况
- 在 2.1.211 版本的解析器里,有四种方式能禁用更新:上面提到的三个环境变量,外加配置文件中的
autoUpdates: false。这四个选项同样会阻止插件更新。 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC是最让人意外的那个:本想用它来控制隐私/出口流量,结果却悄悄地锁死了你的 CLI 版本并冻结了所有插件。autoUpdates: false有可能直接被忽略。这个配置项的路径上还有一个额外的条件判断:
// v2.1.211,原文如此 —— `t` 是设置对象压缩后的名称;
// 上面的每个键都是真实的,因为属性名不会被压缩。`!1`/`!0` 对应 false/true。
if(t.autoUpdates===!1&&(t.installMethod!=="native"||t.autoUpdatesProtectedForNative!==!0))return{type:"config"}
// ^ 在原生安装且 autoUpdatesProtectedForNative 为 true 的情况下,
// 你的 `autoUpdates: false` 根本不会生效
-
因此,这个配置键不仅没有文档说明——
autoUpdates(与autoUpdatesChannel不同)在官方文档中完全找不到——而且还会被条件覆盖。建议优先使用环境变量:它们会在配置之前被检查,且无条件生效。 -
在 2.1.98 版本中标记为
fixed,DISABLE_AUTOUPDATER并未完全阻止 npm 安装模式下的 npm 注册表版本检查和符号链接修改——很长一段时间里,“已禁用”其实并未真正禁用。 - 自动更新程序会在每次发布时覆盖
~/.local/bin/claude下的自定义启动器/符号链接(该问题已修复;/doctor现在会标记外部管理的启动器)。 - 不同安装方式的自更新行为各不相同:原生安装和 npm 安装默认会自动更新;Homebrew、WinGet、apt、dnf 和 apk 则不会(Homebrew 和 WinGet 可通过
CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE=1选择开启)。如果你用的是原生或 npm 安装,除非你另行设置,否则就会跟着日常更新节奏走。
Claude 的研究到此结束。
#总结
虽然源代码并未完全公开,但我惊讶地发现,我们仍能从中学到这么多。知道要关注哪些线索当然有帮助,但这依然无法替代一份编写得当、准确的变更日志。
这不是一行不小心漏进来的代码。它看起来像是一个有意的功能。为什么它会在完全不被察觉的情况下发布,至今令人费解。
我并不认为 Anthropic 是恶意的。事情总会发生。但这类事情是否还会继续发生,将帮助我们判断 Anthropic 是否从这次错误功能中吸取了教训。
- 关于冷却时间和 Dependabot 调优
- .claude 的攻击面
- Claude 总会找到办法