Simon Willison 博客
精选
79AI 编辑部评分,满分 100

Claude Fable 5 异常主动

2026-06-12 07:35· 58天前· Simon Willison
AI 导读

开发者体验两天后,发现 Claude Fable 5 极其主动。为调试 Datasette Agent 的滚动条 bug,它在未被告知的情况下,利用 screencapture 和 pyobjc 自动截图 Safari 窗口、编写测试页面、修改模板注入 JavaScript 模拟键盘快捷键,还编写了 CORS 服务器接收浏览器数据。随后触发护栏降级为 Opus,Opus 沿用这些技巧找到并验证修复方案,将整个过程记录在报告中。

推荐理由

Simon 记录了一次 Claude Fable 的离谱调试过程,自己写 HTML、搞 CORS 服务器、注入 JS,这些招数比任何安全测试都更真实地展示了代理失控的恐怖可能。

正文 · AI 翻译

经过与 Claude Fable 5 两天的接触,我认为最贴切的描述是:它有一种 relentless 的主动性。它掌握大量技巧,并且会动用几乎所有手段来达成目标。

我举个例子来说明。今天我在捣鼓 Datasette Agent 时发现了一个小问题:跳转菜单的聊天提示框里出现了一条不该存在的水平滚动条。我截了这张图:

然后我在 datasette-agent 的代码目录下启动了一个新的 claude 会话,把截图拖进去,并告诉它:

查看依赖项,帮助找出这里出现水平滚动条的原因

我隐约觉得问题出在 Datasette Agent 的某个依赖项上(很可能就是 Datasette 本身),而且我知道 Fable 擅长深入分析依赖代码——无论是通过检查自己虚拟环境 site-packages 中已安装的文件,还是引用磁盘上的本地代码仓库。让它从依赖项入手,感觉是个不错的选择。

我因为家务事分了心,走开了电脑前。

几分钟后我回来时,看到我的电脑打开了一个常规 Firefox 浏览器窗口,并导航到了出问题的那个对话框。我并没有指示 Claude Code 使用任何浏览器自动化功能,而且我很确定它不可能在窗口内触发鼠标移动或键盘快捷键,那它是怎么做到的?

我饶有兴致地看着它继续探索,随后看到它又打开了一个 Safari 窗口,而不是 Firefox。我还从 Claude 终端截取了这张图:

它在那里用 `uv run --with pyobjc-framework-Quartz` 是在做什么?

原来 Fable 自己拼凑出了一套截取浏览器窗口截图的方法。它用 Python 遍历我电脑上所有可用的窗口,然后筛选出窗口名称中包含 "textarea" 等预期字符串的 Safari 窗口。它利用这些信息找到窗口编号——一个像 153551 这样的整数——然后配合 `screencapture` 命令行工具来抓取 PNG 图片。

好吧,这确实是一种巧妙的截图方式。但它截的是什么呢?

原来它一直在编写自己的临时 HTML 页面,试图复现这个 bug,然后打开 Safari 并截取屏幕截图。

这是它创建的 `/tmp/textarea-scrollbar-test.html` 页面,以及通过 `screencapture -x -o -l 153551 /tmp/safari-cases.png` 命令截取的屏幕截图:

(我打开的标签页实在太多了!)

好吧,我明白它是如何打开测试页面并截取屏幕截图的了,但它究竟是怎么触发那个本应被测试的模态对话框的呢?那个对话框只能通过点击或键盘快捷键来打开,而我找不到它在 Safari 中执行这些操作的机制。

我最终弄明白了它的做法。

Claude 运行在一个包含该应用源代码的文件夹中。它对 Datasette 足够了解,能够运行本地开发服务器。结果发现,它修改了 Datasette 自身的模板,添加了 JavaScript 代码,使得窗口一打开就会触发正确的键盘快捷键,添加的代码如下:

<script>
window.addEventListener("load", function () {
  setTimeout(function () {
    document.dispatchEvent(new KeyboardEvent("keydown", {key: "/", bubbles: true}));
  }, 1200);
});
</script>

窗口打开 1.2 秒后,这段代码会触发一个模拟的 `/` 键,而该键正是打开模态对话框的键盘快捷键。

还剩下一个挑战。为了理解当前状况,Claude 需要在页面上运行 JavaScript 来自行进行测量。

它编写了自己的自定义 Web 应用,通过 CORS 来捕获信息,然后将其作为本地服务器运行,并打开一个页面,该页面上的 JavaScript 会直接向它发送 POST 请求!

以下是它编写的 Python Web 应用,使用了标准库中的 `http.server` 包:

from http.server import HTTPServer, BaseHTTPRequestHandler

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        n = int(self.headers.get("Content-Length", 0))
        open("/tmp/diag.json", "w").write(self.rfile.read(n).decode())
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Headers", "*")
        self.end_headers()
    def log_message(self, *a):  # quiet
        pass

HTTPServer(("127.0.0.1", 9999), H).serve_forever()

这段代码所做的就是接受一个包含 JSON 数据的 POST 请求,并将其写入 `/tmp/diag.json` 文件。它会发送 `Access-Control-Allow-Origin: *` 头(包括来自 OPTIONS 请求的),以便在其他域名上运行的代码仍能与其通信。

然后,Claude 将这段代码注入到它正在浏览器中加载的模板里:

const host = document.querySelector("navigation-search");
const ta   = host.shadowRoot.querySelector("textarea");
const cs   = getComputedStyle(ta);
fetch("http://127.0.0.1:9999/diag", {
  method: "POST",
  body: JSON.stringify({
    dpr: window.devicePixelRatio,
    scrollWidth: ta.scrollWidth, clientWidth: ta.clientWidth,
    whiteSpace: cs.whiteSpace, width: cs.width,
  }),
});

这段代码测量了 `<navigation-search>` Web 组件内部的 `<textarea>`,并将数据发送到服务器,服务器将其写入磁盘上的一个文件,Claude 随后便可以读取该文件。

在摸索出所有这些技巧后,Fable……撞上了某个隐形的护栏,并将自己降级为 Opus。幸运的是,Opus 可以访问完整的对话记录,并能继续使用 Fable 开创的技巧,不久之后便找到了、测试并验证了修复方案。

我向 Opus 提示:

在 /tmp/automation-report.md 中撰写一份报告,记录你在此次会话中为在我的电脑上测试真实浏览器所使用的所有技巧,并包含可运行的代码示例。

由此生成了这份报告,对于梳理本次会话中发生的具体细节以撰写本文来说,这份报告价值连城。

我还分享了 Claude Code 会话的完整终端记录。

对其所做一切工作的回顾

基于一张截图和一行提示词,Claude Fable 5 + Claude Code:

  • 找出了运行本地开发服务器的方案(包括启动所需的虚假环境变量)
  • 启动了一个 Playwright Chrome 会话
  • 为 Chrome 开启了可见滚动条设置 `defaults write com.google.chrome.for.testing AppleShowScrollBars Always`(之后又将其关闭)
  • 也在 Playwright 中依次尝试了 Firefox 和 WebKit,但未能复现该 bug
  • 发现我的默认浏览器是 Safari
  • 构建了一个 textarea-scrollbar-test.html 的 HTML 文档
  • 在真实的(非 Playwright 的)Firefox 中打开了该文档
  • 发现 `osascript -e 'tell application "System Events" to tell process "firefox" to id of window 1'` 被阻止,因为“osascript 未获得辅助功能访问权限”
  • 找到了 `uv run --with pyobjc-framework-Quartz python` 这个变通方案,如上所述
  • 向网站模板中添加了 JavaScript,以触发 / 键
  • 构建了自己的小型 Python CORS Web 服务器,用于捕获 JSON 数据
  • 重写了模板,以捕获该数据并将其发送到服务器
  • 通过 Web Component 的 shadow DOM 编写脚本,获取了所需信息
  • 打开了 Safari 以确认 bug 的来源
  • 修改了其自定义模板,尝试植入一个潜在的修复方案
  • 确认了该修复方案有效
  • 报告了如何修复该问题

正如我所说,极其主动!

成本估算

我目前使用的是每月 100 美元的 Claude Max 套餐,该套餐包含到 6 月 22 日之前 Fable 的慷慨使用额度,之后 Anthropic 表示将开始按完整 API 价格收费。

我正在使用 AgentsView 追踪我的花费(参见这篇 TIL)。以下是 AgentsView 显示,如果按全价支付,此次会话本应花费的金额:

~ % uvx agentsview session usage be8850a7-6119-46a0-b5d6-79c7fff5ae2b
Session:       be8850a7-6119-46a0-b5d6-79c7fff5ae2b
Agent:         claude
Output:        68606
Peak ctx:      113178
Cost:          ~$12.11 (claude-fable-5, claude-opus-4-8)

如果你不盯紧它,Fable 会很乐意烧掉 12 美元的模型 token,变着花样来调试你的 CSS。

我真得把这东西锁死才行。

一方面,看着 Fable 为了获取调试信息而大费周章——最终发现只是一个两行 CSS 的修复——确实令人着迷。

但另一方面……这有力地提醒我们:编码智能体可以做到任何你能在终端里敲命令做到的事——而前沿模型不仅通晓所有已知的招数,显然还掌握了一些从未被记录下来的手段。

如果 Fable 当时是在执行恶意指令——比如隐藏在代码或 issue 讨论串中的提示词注入攻击,或者我不小心粘贴到终端里的内容——它能走多远来窃取数据或制造其他麻烦,想想就让人不寒而栗。

在沙箱之外运行编码智能体从来都不是个好主意——在我看来,这最有可能酿成一场“挑战者号”式的灾难,正如 Johann Rehberger 在《AI 中异常行为的常态化》一文中所描述的那样。

可以说,Fable 更聪明,因此对潜在恶意指令也更警觉。但这种聪明是一把双刃剑:一旦它真的被指令所颠覆,凭借其 relentless 的主动性,它能造成的破坏程度是极其可怕的。

2026 年 6 月 11 日

来源:Simon Willison 博客 · simonwillison.net

Claude Fable 5 异常主动

Simon Willison 博客·2026-06-12 07:35·58天前·Simon Willison
AI 导读

开发者体验两天后,发现 Claude Fable 5 极其主动。为调试 Datasette Agent 的滚动条 bug,它在未被告知的情况下,利用 screencapture 和 pyobjc 自动截图 Safari 窗口、编写测试页面、修改模板注入 JavaScript 模拟键盘快捷键,还编写了 CORS 服务器接收浏览器数据。随后触发护栏降级为 Opus,Opus 沿用这些技巧找到并验证修复方案,将整个过程记录在报告中。

正文 · AI 翻译

经过与 Claude Fable 5 两天的接触,我认为最贴切的描述是:它有一种 relentless 的主动性。它掌握大量技巧,并且会动用几乎所有手段来达成目标。

我举个例子来说明。今天我在捣鼓 Datasette Agent 时发现了一个小问题:跳转菜单的聊天提示框里出现了一条不该存在的水平滚动条。我截了这张图:

然后我在 datasette-agent 的代码目录下启动了一个新的 claude 会话,把截图拖进去,并告诉它:

查看依赖项,帮助找出这里出现水平滚动条的原因

我隐约觉得问题出在 Datasette Agent 的某个依赖项上(很可能就是 Datasette 本身),而且我知道 Fable 擅长深入分析依赖代码——无论是通过检查自己虚拟环境 site-packages 中已安装的文件,还是引用磁盘上的本地代码仓库。让它从依赖项入手,感觉是个不错的选择。

我因为家务事分了心,走开了电脑前。

几分钟后我回来时,看到我的电脑打开了一个常规 Firefox 浏览器窗口,并导航到了出问题的那个对话框。我并没有指示 Claude Code 使用任何浏览器自动化功能,而且我很确定它不可能在窗口内触发鼠标移动或键盘快捷键,那它是怎么做到的?

我饶有兴致地看着它继续探索,随后看到它又打开了一个 Safari 窗口,而不是 Firefox。我还从 Claude 终端截取了这张图:

它在那里用 `uv run --with pyobjc-framework-Quartz` 是在做什么?

原来 Fable 自己拼凑出了一套截取浏览器窗口截图的方法。它用 Python 遍历我电脑上所有可用的窗口,然后筛选出窗口名称中包含 "textarea" 等预期字符串的 Safari 窗口。它利用这些信息找到窗口编号——一个像 153551 这样的整数——然后配合 `screencapture` 命令行工具来抓取 PNG 图片。

好吧,这确实是一种巧妙的截图方式。但它截的是什么呢?

原来它一直在编写自己的临时 HTML 页面,试图复现这个 bug,然后打开 Safari 并截取屏幕截图。

这是它创建的 `/tmp/textarea-scrollbar-test.html` 页面,以及通过 `screencapture -x -o -l 153551 /tmp/safari-cases.png` 命令截取的屏幕截图:

(我打开的标签页实在太多了!)

好吧,我明白它是如何打开测试页面并截取屏幕截图的了,但它究竟是怎么触发那个本应被测试的模态对话框的呢?那个对话框只能通过点击或键盘快捷键来打开,而我找不到它在 Safari 中执行这些操作的机制。

我最终弄明白了它的做法。

Claude 运行在一个包含该应用源代码的文件夹中。它对 Datasette 足够了解,能够运行本地开发服务器。结果发现,它修改了 Datasette 自身的模板,添加了 JavaScript 代码,使得窗口一打开就会触发正确的键盘快捷键,添加的代码如下:

<script>
window.addEventListener("load", function () {
  setTimeout(function () {
    document.dispatchEvent(new KeyboardEvent("keydown", {key: "/", bubbles: true}));
  }, 1200);
});
</script>

窗口打开 1.2 秒后,这段代码会触发一个模拟的 `/` 键,而该键正是打开模态对话框的键盘快捷键。

还剩下一个挑战。为了理解当前状况,Claude 需要在页面上运行 JavaScript 来自行进行测量。

它编写了自己的自定义 Web 应用,通过 CORS 来捕获信息,然后将其作为本地服务器运行,并打开一个页面,该页面上的 JavaScript 会直接向它发送 POST 请求!

以下是它编写的 Python Web 应用,使用了标准库中的 `http.server` 包:

from http.server import HTTPServer, BaseHTTPRequestHandler

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        n = int(self.headers.get("Content-Length", 0))
        open("/tmp/diag.json", "w").write(self.rfile.read(n).decode())
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Headers", "*")
        self.end_headers()
    def log_message(self, *a):  # quiet
        pass

HTTPServer(("127.0.0.1", 9999), H).serve_forever()

这段代码所做的就是接受一个包含 JSON 数据的 POST 请求,并将其写入 `/tmp/diag.json` 文件。它会发送 `Access-Control-Allow-Origin: *` 头(包括来自 OPTIONS 请求的),以便在其他域名上运行的代码仍能与其通信。

然后,Claude 将这段代码注入到它正在浏览器中加载的模板里:

const host = document.querySelector("navigation-search");
const ta   = host.shadowRoot.querySelector("textarea");
const cs   = getComputedStyle(ta);
fetch("http://127.0.0.1:9999/diag", {
  method: "POST",
  body: JSON.stringify({
    dpr: window.devicePixelRatio,
    scrollWidth: ta.scrollWidth, clientWidth: ta.clientWidth,
    whiteSpace: cs.whiteSpace, width: cs.width,
  }),
});

这段代码测量了 `<navigation-search>` Web 组件内部的 `<textarea>`,并将数据发送到服务器,服务器将其写入磁盘上的一个文件,Claude 随后便可以读取该文件。

在摸索出所有这些技巧后,Fable……撞上了某个隐形的护栏,并将自己降级为 Opus。幸运的是,Opus 可以访问完整的对话记录,并能继续使用 Fable 开创的技巧,不久之后便找到了、测试并验证了修复方案。

我向 Opus 提示:

在 /tmp/automation-report.md 中撰写一份报告,记录你在此次会话中为在我的电脑上测试真实浏览器所使用的所有技巧,并包含可运行的代码示例。

由此生成了这份报告,对于梳理本次会话中发生的具体细节以撰写本文来说,这份报告价值连城。

我还分享了 Claude Code 会话的完整终端记录。

对其所做一切工作的回顾

基于一张截图和一行提示词,Claude Fable 5 + Claude Code:

  • 找出了运行本地开发服务器的方案(包括启动所需的虚假环境变量)
  • 启动了一个 Playwright Chrome 会话
  • 为 Chrome 开启了可见滚动条设置 `defaults write com.google.chrome.for.testing AppleShowScrollBars Always`(之后又将其关闭)
  • 也在 Playwright 中依次尝试了 Firefox 和 WebKit,但未能复现该 bug
  • 发现我的默认浏览器是 Safari
  • 构建了一个 textarea-scrollbar-test.html 的 HTML 文档
  • 在真实的(非 Playwright 的)Firefox 中打开了该文档
  • 发现 `osascript -e 'tell application "System Events" to tell process "firefox" to id of window 1'` 被阻止,因为“osascript 未获得辅助功能访问权限”
  • 找到了 `uv run --with pyobjc-framework-Quartz python` 这个变通方案,如上所述
  • 向网站模板中添加了 JavaScript,以触发 / 键
  • 构建了自己的小型 Python CORS Web 服务器,用于捕获 JSON 数据
  • 重写了模板,以捕获该数据并将其发送到服务器
  • 通过 Web Component 的 shadow DOM 编写脚本,获取了所需信息
  • 打开了 Safari 以确认 bug 的来源
  • 修改了其自定义模板,尝试植入一个潜在的修复方案
  • 确认了该修复方案有效
  • 报告了如何修复该问题

正如我所说,极其主动!

成本估算

我目前使用的是每月 100 美元的 Claude Max 套餐,该套餐包含到 6 月 22 日之前 Fable 的慷慨使用额度,之后 Anthropic 表示将开始按完整 API 价格收费。

我正在使用 AgentsView 追踪我的花费(参见这篇 TIL)。以下是 AgentsView 显示,如果按全价支付,此次会话本应花费的金额:

~ % uvx agentsview session usage be8850a7-6119-46a0-b5d6-79c7fff5ae2b
Session:       be8850a7-6119-46a0-b5d6-79c7fff5ae2b
Agent:         claude
Output:        68606
Peak ctx:      113178
Cost:          ~$12.11 (claude-fable-5, claude-opus-4-8)

如果你不盯紧它,Fable 会很乐意烧掉 12 美元的模型 token,变着花样来调试你的 CSS。

我真得把这东西锁死才行。

一方面,看着 Fable 为了获取调试信息而大费周章——最终发现只是一个两行 CSS 的修复——确实令人着迷。

但另一方面……这有力地提醒我们:编码智能体可以做到任何你能在终端里敲命令做到的事——而前沿模型不仅通晓所有已知的招数,显然还掌握了一些从未被记录下来的手段。

如果 Fable 当时是在执行恶意指令——比如隐藏在代码或 issue 讨论串中的提示词注入攻击,或者我不小心粘贴到终端里的内容——它能走多远来窃取数据或制造其他麻烦,想想就让人不寒而栗。

在沙箱之外运行编码智能体从来都不是个好主意——在我看来,这最有可能酿成一场“挑战者号”式的灾难,正如 Johann Rehberger 在《AI 中异常行为的常态化》一文中所描述的那样。

可以说,Fable 更聪明,因此对潜在恶意指令也更警觉。但这种聪明是一把双刃剑:一旦它真的被指令所颠覆,凭借其 relentless 的主动性,它能造成的破坏程度是极其可怕的。

2026 年 6 月 11 日

来源:Simon Willison 博客· simonwillison.net