Claude Fable 持续主动出击
Claude Fable is relentlessly proactive
基于一张截图和一行提示,Claude Fable 5 通过 Claude Code 在真实浏览器中调试 Datasette Agent 的水平滚动条 bug。它启动 Playwright Chrome 会话,修改 Datasette 模板注入 JavaScript 模拟键盘快捷键触发模态框,编写 Python CORS Web 服务器捕获 Web Component 的 shadow DOM 测量数据,并使用 `pyobjc-framework-Quartz` 和 `screencapture` 遍历窗口截图。最终找到两行 CSS 修复方案。Fable 因护栏限制降级到 Opus,Opus 继续完成修复并生成自动化报告。
经过两天对 Claude Fable 5 的使用体验,我认为最贴切的描述是"极度主动"。它掌握大量技巧,并且会动用几乎所有手段来达成目标。我举个例子来说明。
今天我在捣鼓 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,
}),
});
这段代码测量了 Web Component 内部的属性,并将它们发送到服务器,服务器将其写入磁盘上的一个文件,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.htmlHTML 文档 - 在真实的(非 Playwright)Firefox 中打开它
- 发现
osascript -e 'tell application "System Events" to tell process "firefox" to id of window 1'被阻止,因为 "osascript is not allowed assistive access" - 想出了上面描述的
uv run --with pyobjc-framework-Quartz python变通方案 - 向网站模板添加了 JavaScript 代码以触发
/键 - 构建了自己的小型 Python CORS Web 服务器来捕获 JSON 数据
- 重写模板以捕获数据并发送到服务器
- 通过脚本遍历 Web Component 的 shadow DOM 以获取所需信息
- 打开 Safari 以确认 bug 的根源
- 修改其自定义模板以植入潜在的修复方案
- 确认植入的修复方案有效
- 报告了如何修复该问题
就像我说的,极度主动!
我真的需要把这个东西锁起来
一方面,看着 Fable 不遗余力地获取调试信息——最终只是一个两行 CSS 的修复——确实令人着迷。但另一方面……这有力地提醒我们,编码代理(coding agent)可以做到你通过终端输入命令能做的任何事情——而前沿模型(frontier model)知道书中的所有技巧,显然还有一些从未有人记录过的技巧。
如果 Fable 当时是在执行恶意指令——隐藏在代码或问题线程中的 prompt injection(提示注入)攻击,或者我不小心粘贴到终端里的东西——它能走多远来窃取数据或造成其他形式的破坏,想想都令人不安。
在沙箱之外运行编码代理一直是个坏主意——这是我认为最有可能引发"挑战者号"式灾难的候选者,正如 Johann Rehberger 在《AI 中异常行为的常态化》中所描述的那样。Fable 可以说更聪明,因此对潜在的恶意指令也更加怀疑。但这种聪明是一把双刃剑:如果它确实被指令颠覆,考虑到它那极度主动的特性,它能造成的破坏程度是可怕的。
标签:ai, prompt-injection, generative-ai, llms, ai-assisted-programming, coding-agents, claude-code, claude-mythos