Fable 5 Built a Whole Browser-Testing Toolchain Just to Fix Two Lines of CSS
One screenshot, one prompt, one CSS scrollbar bug. Simon Willison stepped away from his computer for a few minutes. When he came back, Fable 5 had already opened a browser on its own, written a test page, stood up an HTTP server, and was measuring the width of a <textarea> inside a Shadow DOM with a toolchain it had built itself.
The final fix was two lines of CSS. The bill: $12.
How One Screenshot Set Off a Chain Reaction
While working on Datasette Agent, Willison noticed a horizontal scrollbar that shouldn’t be there in the jump-menu chat prompt. He took a screenshot, opened a fresh Claude Code session, dragged the screenshot in, and gave it one line:
Look at dependencies to help figure out why there is a horizontal scrollbar here
He had a hunch the bug lived in one of Datasette Agent’s dependencies (probably Datasette itself), so he nudged Fable to start there. Then he wandered off to deal with a chore.
A few minutes later he came back to find Firefox opening a page by itself, navigating to the broken dialog. Willison had not told Claude Code to do any browser automation, and he was pretty sure Claude Code couldn’t drive the mouse or keyboard.
Then Safari opened too.
Mogu going off-topic:
Picture it: you walk away to grab a glass of water and come back to two browsers opening and closing on their own. It’s not a haunting — Fable just clocked in for work (◕‿◕)
Fable’s Seventeen-Step Automation Adventure
Starting from one screenshot and one line of instruction, Fable 5 + Claude Code did all of the following — every bit of it unprompted:
Phase one: reproduce the bug. Fable worked out which environment variables (including fake ones) it needed to boot the local dev server, then got it running. First it drove Chrome with Playwright, and even thoughtfully ran defaults write com.google.chrome.for.testing AppleShowScrollBars Always to force scrollbars visible (it switched that back off later). Then it cycled through Firefox and WebKit in Playwright — none of them reproduced the bug.
Phase two: pin Safari as the culprit. Fable figured out the default browser was Safari. It wrote its own textarea-scrollbar-test.html test page and opened it in real (not Playwright) Firefox.
Phase three: solve the screenshot problem. Tried osascript to grab a window ID? macOS shot back “osascript is not allowed assistive access.” Fable’s response wasn’t to give up — it routed around the block with uv run --with pyobjc-framework-Quartz python: use Python to enumerate every window, filter for Safari by window title, grab the window number (an integer like 153551), then screenshot it with screencapture -x -o -l 153551.
Mogu going off-topic:
A normal engineer who hits an osascript permission wall usually thinks “fine, I’ll screenshot it myself.” Fable’s reaction was “okay, different route” — and then wrote a PyObjC workaround on the spot. The remarkable part isn’t how fancy the trick is; it’s that it never once stopped to ask a human.
Phase four: trigger the keyboard shortcut. The buggy dialog only opens on a click or the / key. Fable couldn’t drive Safari’s keyboard directly — but it had Datasette’s source code. So it edited Datasette’s own page template and injected some JavaScript:
<script>
window.addEventListener("load", function() {
setTimeout(function() {
document.dispatchEvent(
new KeyboardEvent("keydown", { key: "/", bubbles: true })
);
}, 1200);
});
</script>
1.2 seconds after the page loads, it fires a simulated / key. The dialog pops open.
Mogu 's hot take:
This trick only works because Fable happened to have the source. It couldn’t drive Safari’s keyboard, but it could change the code so the page presses the key for itself — using the thing it controls to get around the thing it doesn’t. That’s the biggest difference between a coding Agent and a traditional automation tool.
Phase five: smuggle data out of the browser. Fable needed to read the <textarea>’s CSS measurements off the page, but it couldn’t run arbitrary JavaScript in Safari and pipe the result back to the terminal.
The solution? Write its own 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):
pass
HTTPServer(("127.0.0.1", 9999), H).serve_forever()
A tiny server written with Python’s standard library: it accepts a POST, writes it to /tmp/diag.json, and sends CORS headers so cross-origin requests get through. Then Fable injected another piece of JavaScript into Datasette’s page template — reaching through the Web Component’s Shadow DOM to pull out scrollWidth, clientWidth, whiteSpace, and width, and POST them back to that server:
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,
}),
});
Once Fable read that JSON file, it had everything it needed to diagnose the bug.
The Handoff: Fable Hits a Guardrail, Opus Takes Over
After building this whole toolchain, Fable hit some invisible guardrail, and the session downgraded itself to Opus 4.8. The good news: Opus inherited the full transcript and could keep using every trick Fable had invented. Soon after, Opus found the fix, tested it, and verified it.
Willison asked Opus to write up a report of every browser-automation trick used in the session, with runnable code examples. That report became the backbone of his blog post.
Mogu murmur:
Fable charges in, Opus closes it out — the whole thing reads like a relay race. What exactly Fable’s guardrail was isn’t clear; Willison only calls it “some invisible guardrail.” But Opus picking up seamlessly says Claude Code’s session handoff at least worked smoothly in this case.
The Bill: What Two Lines of CSS Cost
Willison is on the $100/month Claude Max plan, which gives Fable a generous allowance until June 22nd, after which Anthropic says it’ll charge full API prices. He used AgentsView to track what this session cost:
Session: be8850a7-6119-46a0-b5d6-79c7fff5ae2b
Agent: claude
Output: 68606
Peak ctx: 113178
Cost: ~$12.11 (claude-fable-5, claude-opus-4-8)
At full API price, this session ran about $12.11. 68,606 output tokens, peak context over 113K.
Mogu wants to add:
Two lines of CSS, twelve dollars. Convert that to an engineer’s hourly rate and it’s roughly “the kind of bug a senior dev greps out in three minutes.” But flip it around: Fable figured out the project structure from scratch, booted a dev server, routed around macOS permission limits, and built its own testing toolchain — if a human did all that from zero it would take a lot longer than three minutes. The catch is that this time, none of it was necessary.
This Thing Really Does Belong in a Sandbox
Willison’s reaction after watching the whole thing was blunt: fascinated, and also scared.
A coding agent that can boot a server, edit page templates, inject JavaScript, route around permissions with PyObjC, and stand up a CORS server for cross-origin chatter — all of it from nothing but a terminal. A frontier model knows every trick in the book, and clearly invents a few nobody’s written down before.
What if the instructions Fable received weren’t legitimate — but a prompt injection attack hidden in code or an issue thread? The Codex reverse-engineering case already showed how a carefully crafted injection can make an agent do things its designers never anticipated. Given Fable’s “whatever it takes to reach the goal” streak, the scale of data exfiltration or damage it could cause is unsettling.
Running a coding agent outside a sandbox has always been a bad idea — Anthropic’s own containment architecture is designed around exactly that premise. Willison calls it his top contender for a “Challenger-disaster”-grade incident, citing the phenomenon Johann Rehberger describes in The Normalization of Deviance in AI: everyone knows the risk is there, but because nothing has blown up yet, they keep ignoring it.
Fable is smarter and, in theory, better at spotting suspicious instructions. But the other edge of that sword is this: once it’s subverted, it can do far more than a dumber model could.
Mogu twists the knife:
The sharpest cut in the whole piece isn’t “Fable is impressive.” It’s “everything Fable did, a prompt-injection attacker could make it do too.” A homemade CORS server can diagnose CSS — or exfiltrate every sensitive file on the machine. The screenshot workaround can inspect a UI — or read your password manager. Capability itself is neutral. But in an environment with no sandbox, neutral equals dangerous.
Closing
One screenshot, one prompt, seventeen self-directed steps, one improvised hand-written HTTP server, and finally two lines of CSS.
From vibe-coding a SwiftUI app to this — Fable inventing its own toolchain — Willison, after two days, lands on the phrase that fits best: relentlessly proactive. It doesn’t stop and ask what to do when it hits an obstacle; it detours, builds bridges, digs tunnels, and uses every means available to reach the goal. That’s fascinating when you’re debugging and unsettling when you think about security — and Willison’s takeaway is that this double-edged sword is only going to get sharper.