cd /news/ai-agents/kitesurf-now-open-source-cloudflare-… · home › topics › ai-agents › article
[ARTICLE · art-169243] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Kitesurf now open source – Cloudflare's agentic browser on Dioxus Blitz

Cloudflare has open-sourced Kitesurf, an experimental browser engine that runs on Cloudflare Workers and parses HTML, applies CSS, executes page JavaScript and renders screenshots without Chrome or a browser container. Kitesurf speaks the Chrome DevTools Protocol, so CDP clients such as Puppeteer can connect to it at ws://127.0.0.1:8787/devtools/browser, but running it on Cloudflare requires Dynamic Workers, which are part of the Workers Paid plan. The repository builds with Bazel and requires Node.js 22 or newer and pnpm 9.15.4, with local development started via pnpm run dev on port 8787.

read9 min views1 publishedNov 13, 2026
Kitesurf now open source – Cloudflare's agentic browser on Dioxus Blitz
Image: Michielbdejong (auto-discovered)

Kitesurf is an experimental browser engine that runs on Cloudflare Workers. It parses HTML, applies CSS, runs page JavaScript, and renders screenshots without Chrome or a browser container.

Kitesurf speaks the Chrome DevTools Protocol (CDP), so you can use it with CDP clients such as Puppeteer.

Important

Kitesurf requires Cloudflare Dynamic Workers to create isolated workers for pages. Dynamic Workers are part of the Workers Paid plan. Local development uses wrangler dev. A Cloudflare Workers Paid plan is required to run Kitesurf on Cloudflare.

  • Bazelisk , installed asbazel . Bazel builds everything. It downloads the pinned Bazel, Rust toolchains, wasm-bindgen, and binaryen itself.
  • Node.js 22 or newer
  • pnpm 9.15.4, which the repository pins in package.json
  • On macOS, GNU tar (brew install gnu-tar )
  • Rust through rustup , only for editing the vendored crate patches.rust-toolchain.toml pins the same nightly that Bazel uses.

Install pnpm separately if it is not already available:

npm install --global pnpm@9.15.4

Clone the repository and install its dependencies:

git clone https://github.com/cloudflare/kitesurf.git
cd kitesurf
pnpm install --frozen-lockfile

Build and start the local engine:

pnpm run dev

The engine listens on http://127.0.0.1:8787. Visit http://127.0.0.1:8787/health to confirm that it is running.

The root URL shows the landing page. It has a form to open a URL in Chrome DevTools, example pages, and the CDP endpoints for agents and other clients. See Use Chrome DevTools.

pnpm run dev builds the WebAssembly modules and the page and renderer bundles with Bazel, then starts wrangler dev. The first build compiles the Rust core and its vendored crates, which takes a while; later builds reuse Bazel's cache.

Set KITESURF_PORT when port 8787 is already in use:

KITESURF_PORT=8799 pnpm run dev

Install puppeteer-core in the project that will control Kitesurf. Then connect to the browser WebSocket endpoint:

import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: "ws://127.0.0.1:8787/devtools/browser",
});

const page = await browser.newPage();
await page.goto("https://example.com/");
await page.screenshot({ path: "page.png" });

await browser.disconnect();

Use the port from KITESURF_PORT if you selected a different port. HTTP discovery is available at http://127.0.0.1:8787/json/version.

With pnpm run dev, open http://127.0.0.1:8787/ in Chrome. Enter a URL to inspect, or select one of the example pages. DevTools opens in the tab on that page. To go to DevTools directly, open http://127.0.0.1:8787/?url=https://example.com, or http://127.0.0.1:8787/?inspect=1 for a blank page.

If you built the bundled frontend, pnpm run dev serves it from the engine's own origin. See Use the bundled DevTools frontend. Otherwise, the DevTools frontend comes from chrome-devtools-frontend.appspot.com, the build that Google hosts for Chrome. The hosted frontend has these limits:

  • The first time it connects, Chrome asks whether chrome-devtools-frontend.appspot.com can access apps on this device. SelectAllow .
  • If you block this access, DevTools says that the WebSocket disconnected. To allow it again, click the site information icon to the left of the address. Then allow the access and reconnect DevTools.
  • The frontend can connect only to 127.0.0.1 . Thus it does not work with a deployed engine.

You can also use chrome://inspect:

  1. Open chrome://inspect/#devices in Chrome. Type or paste it into the address bar because web pages cannot link to it.
  2. Select Discover network targets .
  3. Select Configure... and add127.0.0.1:8787 .
  4. Under Remote Target , selectinspect next tokitesurf .
  5. Enter a page URL in the address bar above the screencast.

Use the port from KITESURF_PORT when you select a different local port. Chrome discovers targets with plain HTTP and WebSocket connections. The chrome://inspect workflow cannot reach an HTTPS engine. Use it with a local engine instead.

Kitesurf provides a CDP endpoint, not a full Chrome implementation. Some Chrome DevTools features do not work yet. We are working to add more support over time. Puppeteer and other CDP clients can connect to ws://127.0.0.1:8787/devtools/browser.

Kitesurf can serve the Chrome DevTools frontend from its own origin. Then DevTools works without the hosted frontend, its permission prompt and the 127.0.0.1 limit. A deployed engine always uses this copy. The frontend is the prebuilt build that Google publishes for Chromium. The default build does not include it.

The --config=devtools Bazel option adds the frontend to the build. The first build with it downloads the frontend (about 250 MB). Bazel keeps the download in its cache for later builds.

To build the frontend once, run this command from the repository root:

bazel run --config=devtools //packages/engine:dev_build

To include the frontend in every local build, add the option to .bazelrc.user. Git ignores this file. Then pnpm run build and pnpm run dev include the frontend.

echo 'common --config=devtools' >> .bazelrc.user

The build copies the frontend to packages/engine/public/inspector/. Git ignores this directory. A build without the option removes it. To turn the frontend off, remove the line from .bazelrc.user.

  1. Build the frontend as described above.
  2. Start the engine with pnpm run dev . The engine serves the bundled frontend when the build includes it, and the hosted frontend when it does not.
  3. Open http://127.0.0.1:8787/ in Chrome. Enter a URL to inspect, or select one of the example pages. DevTools opens in the tab on that page.

To start on a page, open http://127.0.0.1:8787/?url=https://example.com.

Use the smallest step that fixes the problem:

To copy the frontend files again, delete the copy and build again:

rm -rf packages/engine/public/inspector
bazel run --config=devtools //packages/engine:dev_build

To extract the archive again, for example after you change bazel/devtools_frontend.BUILD , fetch it again and then build:

bazel fetch --force --repo=@devtools_frontend --config=devtools
bazel run --config=devtools //packages/engine:dev_build

Bazel extracts the archive from its cache. To download the archive again from Google, add --repository_cache= to thebazel fetch command.

Do not use bazel clean --expunge for this. It also removes the WebAssembly build and the rest of the Bazel cache.

Two pins select the frontend version:

  • DEVTOOLS_FRONTEND_REV and the values after it inMODULE.bazel select the bundled frontend. The comment above them tells you how to select a new snapshot.
  • KITESURF_INSPECTOR_FALLBACK_URL in thelocal environment ofpackages/engine/wrangler.jsonc selects the hosted frontend. Use the Chrome commit thathttps://chromiumdash.appspot.com/fetch_version?version=<chrome-version> returns inhashes.chromium .

Keep the two pins on the same Chrome version. After you change MODULE.bazel, the next build downloads the new archive.

The pnpm scripts are front ends to Bazel:

pnpm run build                    # bazel run //packages/engine:dev_build
pnpm run check                    # type checks, bundles, and repository checks
pnpm run test:unit                # engine vitest and Rust host tests
pnpm run test:e2e <file-substring>
pnpm run format:check             # oxfmt; pnpm run format rewrites the same files
pnpm run format:rust              # format Rust with Bazel's pinned rustfmt
pnpm run lint                     # oxlint, the custom lint rules, and Rust lint

The Rust formatter rewrites tracked and non-ignored untracked files, excluding paths in .oxfmtrc.json.

pnpm run build copies the Bazel-built WebAssembly, bundles, and RBI files into the source tree, where wrangler dev reads them. Other targets run directly, for example bazel test //tests/e2e:gate for the full e2e suite. Another Bazel workspace can depend on this repository as a module.

Kitesurf combines several open-source Rust projects:

  • Blitz provides HTML parsing, the DOM, layout, and painting. It is licensed underMIT OR Apache-2.0 .
  • Stylo provides CSS parsing, cascading, and computed styles. It is licensed underMPL-2.0 .
  • Boa runs page JavaScript inside WebAssembly. It is licensed underUnlicense OR MIT .
  • Linebender libraries provide text layout and 2D graphics, includingParley ,Vello ,Kurbo , andPeniko . These libraries are licensed underApache-2.0 OR MIT .
  • Krilla generates PDF output. It is licensed underMIT OR Apache-2.0 .

Kitesurf patches its Rust dependencies. The patch series are in scripts/patches/, and Bazel applies them itself.

Warning

Do not deploy Kitesurf without authentication in front of it. Use Cloudflare Access (Cloudflare Zero Trust) or another authentication layer. The engine has no authentication of its own. A person who has the URL can:

  • Open DevTools and browse the web through your engine.
  • Connect Puppeteer and other CDP clients to it.
  • Send outbound requests that come from your Cloudflare account.
  • Use CPU time and Dynamic Workers that your account pays for.

The workers.dev URL is public by default. Its name follows a predictable pattern: kitesurf.<your-subdomain>.workers.dev.

To deploy the engine as a Worker on your Cloudflare account:

Deploy the engine. Run the command from the repository root. In a package directory, use pnpm -w run deploy .

pnpm run deploy

Protect the Worker URL with Cloudflare Access immediately. Follow Manage access to workers.dev . For a custom domain or route, protect it with aself-hosted Access application . 3. Open the Worker URL in Chrome. Access asks you to sign in. Then the landing page opens. Enter a URL to open it in DevTools.

Between steps 1 and 2, the engine is public. To prevent this gap, turn on Access for all Workers in the account before you deploy. The workers.dev page in step 2 tells you how.

The command builds the engine with Bazel and uploads it with Wrangler. Wrangler uses the account of wrangler login, or CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN. The command sends other arguments to wrangler deploy, for example --name <worker-name>. Without --name, the Worker name is kitesurf.

To build the deploy without an upload, run pnpm run deploy --dry-run.

To upload a version without sending traffic to it, run pnpm run deploy:preview. The command runs wrangler versions upload. If the Worker has Version URLs turned on, Wrangler prints the URL of the new version. Version URLs are public too. Protect them with Access as well. Follow Manage access to Version URLs.

The deploy always builds with --config=devtools, so it includes the Chrome DevTools frontend. You do not need .bazelrc.user for it. The first deploy downloads the frontend, as the first local build does. The deploy sets KITESURF_INSPECTOR_BASE_URL to /inspector/inspector.html. To use a different frontend, add --var KITESURF_INSPECTOR_BASE_URL:<url> to the command.

DevTools loads from the Worker's own origin, so its WebSocket sends the Access cookie. A DevTools frontend on another origin cannot connect through Access.

Access blocks a CDP client that has no browser session. To connect Puppeteer:

Create an Access service token . 2. Add a policy with the Service Auth action that allows this token. 3. Send the token headers with the WebSocket connection:

const browser = await puppeteer.connect({
  browserWSEndpoint: "wss://<worker-host>/devtools/browser",
  headers: {
    "CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID,
    "CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET,
  },
});

Keep the service token secret. The token gives the same access as a signed-in user.

The engine configuration includes a Worker binding. Kitesurf uses this binding to start a separate worker for each page and renderer. Cloudflare Dynamic Workers provide this capability, and they require the Workers Paid plan. See the Dynamic Workers pricing documentation before planning a Cloudflare deployment.

Package Description
engine Cloudflare Worker and CDP server
page-script Page DOM and JavaScript runtime
page-renderer Renderer for recorded page scenes
dom-shim DOM APIs inside the page worker
core Rust layout, paint, and JavaScript engine code
renderer-core Rust scene rasterizer for page rendering
scene-host-wasm Local scene rasterizer for the terminal client
kitesurf-cli Terminal client

Read CONTRIBUTING.md before opening an issue or pull request. Report security problems as described in SECURITY.md.

Kitesurf is licensed under Apache 2.0. Bundled fonts, data, and dependencies keep their own licenses. See NOTICE.

── more in #ai-agents 4 stories · sorted by recency
── more on @cloudflare 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/kitesurf-now-open-so…] indexed:0 read:9min 2026-11-13 · —