2026-10-26 · ← News
webframe runs a web page like a program in half a second. Only the walls around it make it a sandbox
Simon Willison had Claude Opus 5.5 put webframe through its paces, a small tool by Jeff Lindsay that runs a web page as a command-line program. For scraping it is roughly as fast as shot-scraper. As a sandbox for untrusted JavaScript it only held up once wrapped in an empty network namespace, bubblewrap and a timeout.
The image could not be loaded.
Simon Willison has published an evaluation of webframe, a tool by Jeff Lindsay that runs a web page as a command-line program. Claude Opus 5.5 wrote the evaluation from Willison’s prompt, and the verdict is twofold: the tool is good enough for scraping, but untrusted code needs firm isolation from the outside.
A 5.3 MB program runs the page in the browser the system already has
You point webframe at a URL or a file and it loads the page in a hidden webview. console.log output goes to stdout and exceptions to stderr. With the -c flag it evaluates each line of stdin in the page. It is written in Go on top of the glaze library, which uses the system browser: WKWebView on macOS, WebKitGTK on Linux and WebView2 on Windows. It does not ship its own engine. According to the report, the whole CLI is about 460 lines of Go and JavaScript and builds to a 5.3 MB binary.
Willison gave the model one prompt: try the tool, build prototypes and evaluate it both as a sandbox and as a replacement for shot-scraper javascript. The repository states explicitly that the text and code were written by a language model. Tests ran on Ubuntu 24.04 with WebKitGTK 2.52 and Xvfb. Startup takes about 0.5 s. The model built three prototypes: a patch for webframe, a shot-scraper clone called wfjs in about 350 lines of Go, and a Python run_js function that it attacked with 20 hostile snippets.
For scraping it keeps pace with Playwright, for agents it offers a light JavaScript engine
On scraping the comparison came out even. Fetching the title of example.com took 1.08 to 1.17 s with wfjs and 1.19 to 1.40 s with shot-scraper on Playwright. wfjs uses about 640 MB of memory including WebKit and Xvfb, against about 456 MB for Chrome plus Python. Every example in the shot-scraper docs that does not rely on a shot-scraper-only flag ran unchanged.
For people building agents, the second half is more interesting. An agent that needs to run generated or downloaded JavaScript gets a real browser engine in webframe, with WASM, crypto.subtle and the DOM, but without fs or process objects. Playwright, by contrast, downloads its own browser. The method deserves attention too. Willison handed the assessment of a new dependency to an agent and got back a patch, prototypes and an attack suite. A report like that is a cheap first filter a tool has to pass before a team starts trusting it.
By default the page gets the network, localhost and unlimited memory
Out of the box, webframe is not a sandbox by itself. The page has full network access including localhost, storage persists between runs and WebKit’s renderer sandbox is off. There are no time or memory limits. A page can also overwrite an internal hook and answer every evaluation with a forged result. Three more things get in the way of scraping: pages whose CSP blocks eval, GitHub among them, reject every evaluation, the viewport is 0×0 and a failed load can hang forever.
Even the bubblewrap patch does not close the gap. WebKit’s network process runs outside it, so it protects against a compromised renderer, not against a page that wants to reach the network. One of the attacks showed this: a redirect via location.href delivered a request to localhost despite a strict CSP. Only an empty network namespace via unshare -rn stopped it. A limit via RLIMIT_AS does not work, because WebKit reserves about 75 GB of virtual memory. All the numbers also come from Linux and three runs each; the report did not test macOS or Windows.
An isolated script world and a cgroup memory limit will decide it
The combination that held up against every attack tried has five layers: an empty network namespace, an ephemeral profile with bubblewrap, a timeout that kills the whole process group, a fresh process per call and a result tagged with a random nonce. Each call takes about 450 ms. For serious use the report also wants a cgroup memory and CPU limit, an isolated script world that glaze does not yet expose, and a long-lived worker to remove the startup wait.
The first signal to watch is whether Lindsay merges the ephemeral profile and sandbox patch upstream. The second is whether glaze exposes an isolated world for scripts. Without it, webframe stays a handy tool for scraping trusted pages and only one layer for untrusted code.
Lilith's verdict
One line with a redirect was enough for the page to knock on localhost, even though a strict CSP banned every fetch. webframe is a nimble, fast engine, but whoever runs untrusted JavaScript in it has to build the cell around it: an empty network, a throwaway profile and a stopwatch that cuts the process off in time.