Claude Code 的设计缺陷分析
技术博主剖析 Claude Code 的几处设计不足,帮助用户理解工具的实际边界和改进空间。
技术博主剖析 Claude Code 的几处设计不足,帮助用户理解工具的实际边界和改进空间。
“Mechanical egg timer”,作者 Hustvedt,采用 CC BY-SA 3.0 许可。图片经填充以适配更宽的画幅;该改编版本同样采用 CC BY-SA 3.0 许可。
更新于 2026-07-18:Hacker News 上出现了一些关于本文的讨论。少数评论者断言整篇文章都是由 LLM 生成的。事实绝非如此。文章正文是我自己写的。不过,格式和版式可能让人很容易得出这一结论。现在,我已经为 LLM 生成的部分添加了特殊格式,因此到这里,希望哪些内容出自真人之手已经一目了然。
我保留了 Claude 的分析,使其独立呈现,但周围的文字都是我自己写的。
2026 年 7 月 1 日加拿大国庆日,Anthropic 向 Claude Code 用户发布了一个令人意外的“彩蛋”:2.1.198 版本中包含一种效率绕过机制,允许智能体在等待人类指示时不受阻塞,直接继续执行。实际上,Claude Code 请求输入后,你有 60 秒的回应时间。如果错过这个窗口,Claude Code 就会“贴心地”执行它认为最合适的操作,然后继续推进。其表现如下:
● Claude asked:
⎿ …
● No response after 60s — continued without an answer
● The user stepped away. I'll proceed with best judgment. My plan:
注意:以上内容逐字取自我自己的一个 claude 会话,只删减了其中的问题。
如果你觉得这种行为出人意料,你并不孤单。让我们考虑一下它可能造成的后果:
做三明治时,你是否必须把笔记本电脑一起带进厨房?如果你在这个时间窗口内离开键盘,会发生什么?
你同时运行着多少个智能体?你有可能同时观察所有智能体吗?如果两个或更多智能体在同一个 60 秒窗口内请求你的输入,又会怎样?
如果智能体做出了错误选择怎么办?在此期间又消耗了多少 token?
如果你正在使用智能体进行部署呢?(是的,我知道,但假如真的如此呢)
发布这个功能时,这些都是你理应考虑的问题,或许你还会在变更日志中记录自己的考量。但如果你根本没有在变更日志中提及这些新的默认行为呢?那岂不是更加令人意外?(剧透:事实确实如此!)
这个故事有一个(某种意义上的)美好结局。快速行动、打破常规,并不一定妨碍快速行动、修复问题。几天之内,修复版本便发布了,但这件事让用户对该产品的信任处于何种境地?
我们已经了解到几件事:
Anthropic 理论上(实际上也是如此)可以按日发布节奏向 Claude Code 加入令人意外的功能
并非每项功能都一定会出现在变更日志中
本不应成为默认行为的功能,可能没有文档说明如何将其关闭
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 发布——报告者认定引入回归问题的版本。没有任何公开提交显示这项变更;该版本唯一的公开痕迹,是发布其说明的机器人提交(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 — issue 尚未关闭时,2.1.199 发布了 24 项变更。依然只字未提此事。
2026-07-03 — 2.1.200 恢复了原来的行为;同样,其唯一的公开痕迹是说明提交(1322e9b)
2026-07-04 18:04 UTC — issue 关闭
该 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
# When did the fix land, and in which version?
git log -1 --format='%h %ai' -S'no longer auto-continue by default' -- CHANGELOG.md
# 1322e9ba 2026-07-03 16:52:26 +0000
# When did the version that shipped the bug land?
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, verbatim. `Thl` is the minifier's name; the prose is the binary's own.
// Note "60s" is interpolated, not a literal in the file:
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, verbatim. `a` is the minifier's name for "some answers exist"
// (`s=Object.entries(r)` over the answers, `a=s.length>0`); both string values
// are the binary's own.
let d=a?"continued with the answers selected so far":"continued without an answer"
因此,回答到一半的对话不会丢弃部分输入——它会直接提交这些输入。三个问题中回答完第一个后离开,超时机制就会提交你的这一个答案,而另外两个问题则采用模型自行选择的答案
这两个字符串在 2.1.197 中均不存在,却同时出现在 2.1.198 中
公平地说:它在屏幕上并非毫无提示。对话框会显示实时倒计时,按下任意键都会重新开始计时。由于它是在运行时组装,而不是以完整字符串的形式存储,所以下面展示的是渲染后的形式,并非通过 grep 直接匹配到的字面值:
// v2.1.198, verbatim — the pieces. `s` is the remaining-seconds value.
children:["auto-continue in ",s,"s \xB7 any key to stay"]
// renders as: auto-continue in 12s · any key to stay
但这项辩解的有效程度远没有看起来那么高。倒计时只能提醒正在看屏幕的人,而该功能的前提恰恰是你没有在看:内部名称是 AFK;消息中写着“用户可能已离开键盘”同时运行多个智能体时,“看着屏幕”并不是只盯着一个地方——你需要关注的倒计时可能正在另一个标签页上
内部名称是 AFK;消息中写着“用户可能已离开键盘”
同时运行多个智能体时,“看着屏幕”并不是只盯着一个地方——你需要关注的倒计时可能正在另一个标签页上
而且倒计时出现得很晚。该阈值默认为 20 秒(CLAUDE_AFK_COUNTDOWN_MS),其判断依据是剩余时间,而不是已经过去的时间——因此,在最初的 40 秒里,该对话框看起来与普通的阻塞式问题完全相同。它确实显示在屏幕上,但上面没有任何内容表明计时器正在运行:
// v2.1.198 ships this minified — the locals are mangled, but the property
// names survive, so `showCountdown` and `remainingSeconds` are its own words:
let u=i*1000<=n;return{remainingSeconds:i,showCountdown:u,timeoutMs:t}
// ...which reads, with n at its default of 20000:
showCountdown = remainingSeconds * 1000 <= 20000
警告直到最后三分之一的时间才会出现
什么它不触及:超时仅适用于 AskUserQuestion。Anthropic 的工具参考文档说"权限提示(包括计划批准)在空闲时永不自动解决"——不同于本文他处文档声称,这条可以与已发布的 2.1.198 代码对比验证,而非修复后编写的页面:倒计时组件(q0m)在整个包中仅有一个调用点,其计时器钩子(_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 钩子。任何针对部署运行智能体的人都可能已将部署命令纳入白名单或关闭了提示——这正是自动化的含义。对他们而言权限层永不会触发,所以其对计时器的豁免毫无价值
较狭隘的主张,也是真正生效的主张:计时器无法授予权限,但它可以做出选择。AskUserQuestion 不是要求权限,而是要求你做决定——"暂存还是生产?"、"哪个配置?"。超时时模型被告知用你的最佳判断继续,部分答案路径继续"使用迄今为止选择的答案"。若权限已由白名单或绕过授予,选择是仅剩的关卡
工具模式中不存在超时参数——模型既无法设置也无法控制它。已在二进制中验证(U_f=…H.strictObject({questions:…})),而非源自议题讨论。输入参数仅为:
questions, answers, annotations, metadata
所以模型说它没有跳过任何东西时是诚实的——工具包返回了答案,而非模型
2.1.200 默认关闭了自动继续
空闲超时现在通过 /config 选择加入,不再强制对所有人启用
注意更改日志的措辞:"不再默认自动继续"。并未删除任何东西——默认值翻转了。
该说明指向的 /config 设置在修复前不存在。在 2.1.198 中搜索 askUserQuestionTimeout,得到零结果:该功能发布时,唯一的逃脱途径是环境变量——发布说明从未提及它。
你可在当前二进制(2.1.211,十一个版本之后)中验证其余部分。机制完全保持不变:
所以修复是条件门中的一行改动,而非删除:
// 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; } }
// ^^^^^^^^^^^^ unset => null => disabled
公平解读:这是正确的修复。选择加入应该从一开始就是如此,该功能对某些人确实有价值。
不太舒适的解读:那段为你自动回答的相同代码仍在发布,仅隔一个配置值,受同一流程管制——该流程在第一次就将其无声打开。
修复前的临时方案(来自讨论),供任何固定在受影响版本的人使用:
// settings.json — 通过设置一个巨大的 AFK 窗口来禁用自动继续
"env": {
"CLAUDE_AFK_TIMEOUT_MS": "<massive number>"
}
周转很快——从报告到反转大约两天(该给的功劳)
两个地方发布发布说明:官方更改日志和仓库中的 CHANGELOG.md(内容相同)
下面链接固定在提交 1322e9b,因此显示当时的说明文件,而非之后编辑的版本
2.1.197 更改日志:一行,Claude Sonnet 5 发布。关于问题超时的内容为空。
2.1.198 更改日志:约 30 项。关于 AskUserQuestion 自动继续的内容为空。
2.1.199 更改日志:24 项,在问题已提出时发布。仍然为空。
60 秒自动继续添加时从未在任何发布说明中宣布
AskUserQuestion 对更改日志并不陌生——它跨越 13 个版本出现 15 次,回溯至 2.0.55。它是 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
- Changed `AskUserQuestion` dialogs to no longer auto-continue by default;
opt into an idle timeout via `/config`
控制环境变量 CLAUDE_AFK_TIMEOUT_MS 在更改日志或 README 中任何地方都未出现
今天,两个环境变量都在环境变量参考中记录。该条目对该事件坦诚:
CLAUDE_AFK_TIMEOUT_MS — 未回答的 AskUserQuestion 对话自动继续前的空闲时间(毫秒)。
自动继续默认关闭;使用 askUserQuestionTimeout 设置选择加入。[...] 在 v2.1.198 和
v2.1.199 中,自动继续默认打开,超时为 60000(60 秒)。
但该文本不可能是 7 月 1 日的内容,二进制本身已证明:它指向 askUserQuestionTimeout 作为选择加入,而该设置在 2.1.198 二进制中零次出现。它也以过去时叙述 2.1.198 和 2.1.199,作为一个已闭合范围
Wayback Machine 决定性地解决了它。文档无公开仓库,我原以为其历史不可知。它可以——该页面被存档多次,直通整个窗口期:
# env 变量参考的每个存档捕获,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 捕获是四小时四十五分钟之后——页面以任何名称都未提及该功能:不是 afk,不是自动继续,不是 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)。而文档条目将自动继续描述为"默认关闭",这仅在修复后才是真实的。文档从未描述 7 月 1 日和 2 日时的实际情况。它们伴随回滚而来,只记录了已回滚的行为。
文件更新后能说得好听点,但实际上它们只是跳过了那个特性处于启用状态的部分。
显而易见的下一步:打开引入该特性的提交并阅读其推理过程。
显而易见的下一步:打开引入该特性的提交并阅读其推理过程。
但它不存在。不是"难以找到"——它根本不存在于公开仓库。
但它不存在。不是"难以找到"——它根本不存在于公开仓库。
也没有回滚提交。2.1.200 的回滚同样没有公开提交。
也没有回滚提交。2.1.200 的回滚同样没有公开提交。
这两个版本发布留下的唯一 git 制品是两个自动化的更新日志提交,都标题为 chore: Update CHANGELOG.md and feed.xml ——关于发布的说明,而非发布本身:
75709ea — 发布 2.1.198 的说明(运送该特性的版本,未被提及)1322e9b — 发布 2.1.200 的说明(回滚)好的,那就比对版本间的源代码。但没有源代码可比。
好的,那就比对版本间的源代码。但没有源代码可比。
anthropic/claude-code 不是产品本身。它是更新日志、文档、插件示例、一些示例基础设施配置,以及分类问题跟踪器的机器人:
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/ 是八个问题跟踪器自动化文件(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 是重新表述为 RSS 的更新日志,所以该比对是更新日志的两份副本。在十个连续版本(2.1.196 → 2.1.206)中,每个标签之间的比对都只涉及这两个文件,别无其他。
每个标签之间的比对都只涉及这两个文件,别无其他。
结论:版本标签的比对就是发布说明。这些是发布说明标签,不是源代码标签——没有版本的代码可以检出。
结论:版本标签的比对就是发布说明。这些是发布说明标签,不是源代码标签——没有版本的代码可以检出。
所以"查看发布说明"失败了(静默更改),"比对仓库"失败了(无源代码),"比对标签"失败了(标签就是说明)。三条死路,一个原因:Anthropic 发布到 git 上的任何东西都不是他们运送给你的东西。
所以"查看发布说明"失败了(静默更改),"比对仓库"失败了(无源代码),"比对标签"失败了(标签就是说明)。三条死路,一个原因:Anthropic 发布到 git 上的任何东西都不是他们运送给你的东西。
编写的源代码没有公布到任何地方;该行为仅在已编译的二进制文件内运行。
编写的源代码没有公布到任何地方;该行为仅在已编译的二进制文件内运行。
所以关于该特性曾存在的公开证据总和是:
CLAUDE_AFK_TIMEOUT_MS),用户通过互相询问找到,没有发布说明提及过关于该特性曾存在的公开证据总和是:
CLAUDE_AFK_TIMEOUT_MS),用户通过互相询问找到,没有发布说明提及过该特性的引入在发布说明或 git 中没有留下任何痕迹。其删除是说明中第一次提及它。
该特性的引入在发布说明或 git 中没有留下任何痕迹。其删除是说明中第一次提及它。
值得区分两个容易混淆的声明:
值得区分两个容易混淆的声明:
如果你愿意,可以跳过:已运送的二进制文件解决了 git 无法回答的问题。该特性在 2.1.197 中确实不存在,在 2.1.198 中存在,你可以在大约五分钟内自己验证这一点。
如果你愿意,可以跳过:已运送的二进制文件解决了 git 无法回答的问题。该特性在 2.1.197 中确实不存在,在 2.1.198 中存在,你可以在大约五分钟内自己验证这一点。
从未发布过官方理由(没有设计文档、没有更新日志行、没有 PR——见下文)
从命名和消息文本推断的意图证据,全部来自于此:内部名称是 AFK——"away f