Playwright build

# Chromium

The open-source Chromium build that ships with Playwright. Fast, predictable and the most compatible choice for automation.

Launch

`POST https://starter.cdpfleet.com/chromium/session`

Connect

`chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } })`

Playwright client

1.60.x

Versions now

**build**148.0.7778.96

## Example

 

```node
// npm install playwright@1.60.0
// Browsers run on cdpfleet, so no `npx playwright install` is needed.
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// 1. Launch the browser
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({
    proxy: 'http://user:pass@proxy.example.com:8080',
    headless: 'new',
  }),
});
if (!res.ok) throw new Error(`launch failed: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();

// 2. Connect and drive it
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close(); // ends the session and stops billing
```

```python
# pip install playwright==1.60.0 requests
# Browsers run on cdpfleet, so no `playwright install` is needed.
import os
import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]

# 1. Launch the browser
res = requests.post(
    "https://starter.cdpfleet.com/chromium/session",
    headers={"x-api-key": KEY},
    json={
        "proxy": "http://user:pass@proxy.example.com:8080",
        "headless": "new",
    },
    timeout=60,
)
res.raise_for_status()
ws_url = res.json()["wsUrl"]

# 2. Connect and drive it
with sync_playwright() as p:
    browser = p.chromium.connect(ws_url, headers={"x-api-key": KEY})
    page = browser.new_page()
    page.goto("https://example.com")
    print(page.title())
    browser.close()  # ends the session and stops billing
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0  and  com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1: browsers run on cdpfleet.
import com.google.gson.JsonParser;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;

public class Example {
  public static void main(String[] args) throws Exception {
    String key = System.getenv("CDPFLEET_API_KEY");

    // 1. Launch the browser
    String body = """
        {
          "proxy": "http://user:pass@proxy.example.com:8080",
          "headless": "new"
        }
        """;
    HttpResponse<String> res = HttpClient.newHttpClient().send(
        HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
            .header("x-api-key", key)
            .header("content-type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build(),
        HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) {
      throw new RuntimeException("launch failed: " + res.statusCode() + " " + res.body());
    }
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    // 2. Connect and drive it
    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl,
          new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", key)));
      Page page = browser.newPage();
      page.navigate("https://example.com");
      System.out.println(page.title());
      browser.close(); // ends the session and stops billing
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// Browsers run on cdpfleet, so no `playwright install` is needed.
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;

// 1. Launch the browser
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var body = """
    {
      "proxy": "http://user:pass@proxy.example.com:8080",
      "headless": "new"
    }
    """;
var res = await http.PostAsync("https://starter.cdpfleet.com/chromium/session",
    new StringContent(body, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new Exception($"launch failed: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

// 2. Connect and drive it
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new BrowserTypeConnectOptions
{
    Headers = new Dictionary<string, string> { ["x-api-key"] = key },
});
var page = await browser.NewPageAsync();
await page.GotoAsync("https://example.com");
Console.WriteLine(await page.TitleAsync());
await browser.CloseAsync(); // ends the session and stops billing
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
//
// One-time driver setup (needs Node.js 18+): playwright-go can no longer download the
// 1.60.0 driver itself, so build it from npm and point PLAYWRIGHT_DRIVER_PATH at it:
//   mkdir -p ~/.cache/pw-driver-1.60.0 && cd ~/.cache/pw-driver-1.60.0 &&
//     curl -sL https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz | tar xz &&
//     ln -sf "$(command -v node)" node
//   export PLAYWRIGHT_DRIVER_PATH=~/.cache/pw-driver-1.60.0
// Browsers run on cdpfleet, so no browsers are downloaded (SkipInstallBrowsers).
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/playwright-community/playwright-go"
)

func main() {
	key := os.Getenv("CDPFLEET_API_KEY")

	// 1. Launch the browser
	body := `{
	  "proxy": "http://user:pass@proxy.example.com:8080",
	  "headless": "new"
	}`
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/session", strings.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		log.Fatalf("launch failed: %s %s", res.Status, msg)
	}
	var session struct {
		WsURL string `json:"wsUrl"`
	}
	if err := json.NewDecoder(res.Body).Decode(&session); err != nil {
		log.Fatal(err)
	}

	// 2. Connect and drive it (Install fetches only the Playwright driver, once)
	opts := &playwright.RunOptions{SkipInstallBrowsers: true}
	if err := playwright.Install(opts); err != nil {
		log.Fatal(err)
	}
	pw, err := playwright.Run(opts)
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()
	browser, err := pw.Chromium.Connect(session.WsURL, playwright.BrowserTypeConnectOptions{
		Headers: map[string]string{"x-api-key": key},
	})
	if err != nil {
		log.Fatal(err)
	}
	page, err := browser.NewPage()
	if err != nil {
		log.Fatal(err)
	}
	if _, err := page.Goto("https://example.com"); err != nil {
		log.Fatal(err)
	}
	title, _ := page.Title()
	fmt.Println(title)
	browser.Close() // ends the session and stops billing
}
```

Want different options or a specific version? Open this browser in the [code builder](https://cdpfleet.com/docs/builder?browser=chromium).

## Launch options

Send these as the JSON body of the launch request. Everything is optional; unknown fields are ignored. Context options such as viewport, user agent or cookies are set client-side after connecting (`browser.newContext({...})`).

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| [`proxy`](https://cdpfleet.com/docs/options/proxy) | `string | object | array` | the free Community proxy | 
A proxy URL (`http://user:pass@host:port`, `https://…`, `socks5://user:pass@host:port`; no scheme means `http://`), an object `{ "server": "http://host:port", "username": "…", "password": "…" }`, or a list of proxies used round-robin as a pool. SOCKS5 with authentication works on every browser.

**Use it for:** Every session that needs a particular exit: your own proxy for the IP and country you choose, a list as a pool to spread requests across IPs, or a `cdpfleet-resi` token for residential IPs billed per GB.

Optional: without a proxy (and without `vpn`) the free Community proxy is used — shared, slower and capped (`503 community_capacity_full` when busy), so bring your own or use the residential proxy for work that matters. Sessions never browse from our servers' own IPs. An unparseable proxy returns 400.

Example: `"http://user:pass@proxy.example.com:8080"`

 |
| [`proxy_rules`](https://cdpfleet.com/docs/options/proxy_rules) | `array` | none | 

A list of `{ "name": "…", "hosts": [...], "proxy": "…" | [...] }` rules. Requests to a host matching a rule use that rule's proxy (round-robin when it is a list); every other host uses `proxy`. Host patterns support `*` wildcards, e.g. `*.cdn.example.com` or `static.*`. The optional `name` (up to 64 characters) labels the rule's traffic on your dashboard's Proxy traffic page and in its CSV export.

**Use it for:** Scrapers that want an expensive exit only where it matters: residential for the target site, a cheaper proxy for images, CDNs and APIs, or a different country per host.

Needs `proxy` to be set as the default route. Rules route through a proxy only; routing a host with no proxy (direct from our servers) is not available. The first matching rule wins — keep rules from overlapping so traffic is attributed to the right one.

Example: `[{"name":"dc-assets","hosts":["*.cloudfront.net","static.example.com"],"proxy":["http://u:p@dc1.example:4444","http://u:p@dc2.example:4444"]}]`

 |
| [`inactivity_timeout`](https://cdpfleet.com/docs/options/inactivity_timeout) | `number | string` | 120s | 

The timer resets on any protocol traffic in either direction. A number of seconds (`90`, `1.5`) or a string with a unit: `500ms`, `45s`, `10m`, `2h` (digits only, no spaces or decimals). Clamped to 1 s – 24 h. An unparseable value silently falls back to the default.

**Use it for:** Freeing the thread quickly when a script dies (short), or giving a person or a slow step time to think (long), up to your plan's cap.

Up to 10 minutes on shared threads, 5 on Community and 24 h on dedicated; a longer value is cut to your plan's maximum. Idle minutes are billed like any others. A slow page load through a proxy can produce no traffic for a while, so keep it at 60 s or more. Never longer than the session's overall timeout.

Example: `"2m"`

 |
| [`overall_timeout`](https://cdpfleet.com/docs/options/overall_timeout) | `number | string` | your plan's maximum session length (24 h) | 

A number of seconds (`90`, `1.5`) or a string with a unit: `500ms`, `45s`, `10m`, `2h` (digits only, no spaces or decimals). Clamped to 1 s – 24 h. An unparseable value silently falls back to the default. cdpfleet also caps it at your plan's maximum session length (24 h) and, for shared threads, at your remaining daily minutes.

**Use it for:** A hard time budget per job, so a stuck loop can't hold a thread for hours: batch scrapers and agents with a deadline.

Example: `"30m"`

 |
| [`memory`](https://cdpfleet.com/docs/options/memory) | `integer` | 1 | 

A multiplier of your plan's per-session memory (Community 2 GB, shared 4 GB, dedicated 6 GB; headful sessions get 512 MB more for the display). The session's weight is multiplied too: headless with `"memory": 2` takes 2 threads, headful 4, and its minutes are used up as fast. The launch response returns the effective `weight` and `memory_mb`.

**Use it for:** Memory-heavy work that would otherwise end with `session_memory_limit`: many tabs at once, big single-page apps, long infinite scrolls, or large PDFs and screenshots.

Only `1`, `2`, `3` or `4` as a JSON number; anything else (e.g. `4096` or `"2x"`) is refused with `400 invalid_memory`. A launch that would go over your threads gets `429 threads_exceeded` as usual, so a Community headful session can't use it. The tab cap (50) stays the same.

Example: `2`

 |
| [`downloads`](https://cdpfleet.com/docs/options/downloads) | `boolean` | false | 

With `true`, every file the browser downloads in this session is kept: after the session ends it appears under `GET /v1/me/downloads` and on the dashboard's Sessions page, for 2 days. Your own code still receives downloads as usual (`download.saveAs()` in Playwright).

**Use it for:** Downloads you can't catch in code: a person clicking in live view or Launch interactive, an AI agent working through the MCP server, a script that crashed or disconnected (`keep_alive`), or reports and exports you want to collect later.

Paid plans (`403 downloads_not_available` on Community). Up to 1 GB and 200 files per session, and 1 GB per account (the oldest files go first); kept 2 days. Not kept: downloads in contexts a raw-CDP client creates itself, and contexts opened with `acceptDownloads: false`. WebKit doesn't treat a `text/plain` response with `Content-Disposition: attachment` as a download.

Example: `true`

 |
| [`vpn_tunnel`](https://cdpfleet.com/docs/options/vpn_tunnel) | `boolean` | false | 

The browser gets a network of its own whose only way out is the session's residential proxy, so UDP works as on a home connection: WebRTC shows the residential exit IP as its public address, and HTTP/3 (QUIC) works. Without it, a proxied session sends no UDP at all — WebRTC shows only an anonymous mDNS name and sites fall back to HTTP/2. The TLS fingerprint and the exit IP are the same either way. It is not a VPN: the residential proxy still opens the connections to the site, so the TCP/IP fingerprint the site sees is the exit device's, as without the tunnel. UDP traffic is billed at the token's tier and listed as host `(udp)` on the Proxy traffic page.

**Use it for:** Work that needs WebRTC or HTTP/3 to look like a home connection, such as video calls or sites that compare the WebRTC address with the page's IP, on a sticky residential exit.

Needs the default `proxy` to be a `cdpfleet-resi` token on Tier 1 or Tier 2 (no `-tier-N` is Tier 1), or, on dedicated plans, your own proxy too (UDP then works if your SOCKS5 proxy relays it; not billed); anything else — Tier 3/4, a pool, your own proxy on other plans — returns `400 tunnel_not_available`. `proxy_rules` still route page traffic by host, but UDP (WebRTC, HTTP/3) can leave through a rule's proxy instead of the default, because a UDP flow carries no host for the rules to match: for WebRTC that matches the page's exit, don't combine the tunnel with `proxy_rules`. A live swap must keep a Tier 1/2 token. Sticky sessions (`-session-…`) work best: with a rotating exit, WebRTC and HTTP/3 may show a different residential IP than the page. Adds about 0.1 s to the launch.

Example: `true`

 |
| [`vpn`](https://cdpfleet.com/docs/options/vpn) | `string | object` | none | 

The name of a VPN saved on the dashboard's Proxies → My VPNs page (`"vpn": "home-wg"`; the server looks the config up at launch, so no secret goes into your code), or the config itself: `{ "type": "wireguard" | "amneziawg" | "singbox" | "openvpn", "config": "<text>", "username": "…", "password": "…" }`. `config` is a wg-quick `.conf` (WireGuard, AmneziaWG incl. Jc/Jmin/Jmax/S1–S4/H1–H4/I1–I5), a sing-box share link (`vless://` incl. Reality, `vmess://`, `trojan://`, `ss://`, `hysteria2://`/`hy2://`, `tuic://`) or outbound JSON (also SSH), or an `.ovpn` file (`username`/`password` for OpenVPN auth). The launch returns once a real HTTPS request through the VPN has worked. All traffic, UDP included, uses the VPN; DNS only through it; IPv6 off; if the VPN drops, the session goes offline — our servers' IP is never used.

**Use it for:** Dedicated-plan users who already run their own VPN (WireGuard, AmneziaWG, OpenVPN, sing-box): the whole browser, UDP included, leaves through it, so pages and WebRTC show the VPN's address.

Dedicated plans only (`403 vpn_requires_dedicated`). Not with `proxy`, `proxy_rules` or a live swap (`400 vpn_not_available`). Config, username and password together up to 64 KB (`400 invalid_vpn_config`). A server that doesn't answer or refuses the credentials returns `400 vpn_connect_failed`. Not billed. Keep the config out of your code: read it from a file or an environment variable. See [Own network](https://cdpfleet.com/docs/proxies#own-network).

Example: `"home-wg"`

 |
| [`headless`](https://cdpfleet.com/docs/options/headless) | `boolean | "new" | "shell"` | false (headful, on a virtual display) | 

`true` or `"new"` runs Chrome's new headless mode; `"shell"` runs the lightweight headless shell. Anything else — including `1` and `"false"` — runs headful on a virtual display, which is harder to detect and is billed as 2 threads (headless is 1).

**Use it for:** Bulk scraping and tests where nobody watches: lower memory and faster launches. Leave it headful (the default) when a site checks for headless tells, or when you want a recording or live view without extra options.

Example: `"new"`

 |
| [`cdp`](https://cdpfleet.com/docs/options/cdp) | `boolean` | false | 

The launch response then carries `cdpUrl` next to `wsUrl`: a browser-level CDP WebSocket for Puppeteer (`puppeteer.connect({ browserWSEndpoint: cdpUrl, headers })`), Playwright's `connectOverCDP(cdpUrl, { headers })` in any language and version, or any raw CDP client (key in the `x-api-key` header or `?api_key=`).

**Use it for:** Puppeteer users and tools that speak raw CDP (Lighthouse, custom DevTools clients), or connecting with Playwright's connectOverCDP to reach Chromium-only features like network throttling and performance traces.

Same session, proxy and limits. You can connect to `wsUrl` and `cdpUrl` at once, but closing either ends the session. Not on Patchright and Rebrowser (their stealth lives in the Playwright layer CDP bypasses), Whale, Camoufox, Firefox or WebKit.

Example: `true`

 |
| [`screencast`](https://cdpfleet.com/docs/options/screencast) | `boolean` | false | 

Not a launch option: the generated program calls `page.screencast.start({ onFrame })`, which sends JPEG frames of the page to your code over the session's own connection whenever it changes — forward them to your UI, a websocket or a file. Needs a Playwright 1.60+ client.

**Use it for:** Watching or recording a headless page from your own code as a stream of JPEG frames, for debugging dashboards or human review, without a full live view.

View-only and only for the client that drives the page. With `"cdp": true`, use CDP `Page.startScreencast` instead. To watch from somewhere else — or take control — see `live_view` and [Live view](https://cdpfleet.com/docs/live-view).

Example: `true`

 |
| [`live_view`](https://cdpfleet.com/docs/options/live_view) | `boolean` | false (headful sessions are always watchable) | 

Headful sessions can always be watched or taken over (`POST /admin/session/live` → a VNC-over-WebSocket `live_url`, or the dashboard's Watch / Take control). Headless Chromium-based sessions need `"live_view": true` at launch; they are then streamed as JPEG frames (`"protocol": "screencast"`).

**Use it for:** A person watching or taking over the session in the browser (a login, a 2FA step, a check that needs a human), or recording a headless session.

Headless Chromium-based browsers only (Camoufox, Firefox and WebKit are watchable when headful, their default). Doesn't give your code CDP access — add `"cdp": true` for that. The session is also [recorded](https://cdpfleet.com/docs/recordings) unless you add `"record": false`. See [Live view](https://cdpfleet.com/docs/live-view).

Example: `true`

 |
| [`screen_size`](https://cdpfleet.com/docs/options/screen_size) | `"WxH" | { width, height }` | the server's display (1280×720 window) | 

Sets the screen the browser runs on, from 640×480 to 3840×2160, e.g. `"1366x768"` or `{ "width": 1920, "height": 1080 }`. Pages see it as `screen.width`/`height`; Chromium windows open at that size (Playwright then resizes each context's window to its viewport — use `viewport: null` to keep the full display, in live view and recordings too); Camoufox pins its fingerprint screen to it unless you pass `screen`.

**Use it for:** Responsive and visual tests at a given resolution, and personas that need a common desktop or laptop screen instead of the server default.

Headful sessions only — a headless launch with `screen_size` returns 400. Playwright contexts report a screen equal to their viewport (1280×720) unless you open them with `viewport: null` (Python `no_viewport=True`, Java `setViewportSize(null)`, C# `ViewportSize = ViewportSize.NoViewport`, Go `NoViewport: playwright.Bool(true)`); Camoufox always reports the display — the nearest real-world screen size no larger than it (exact for common sizes such as 1366×768 or 1920×1080); with `"os": "macos"` use 1440×900 or larger, since no Mac screen is smaller (400 otherwise).

Example: `"1366x768"`

 |
| [`keep_alive`](https://cdpfleet.com/docs/options/keep_alive) | `boolean` | false | 

A disconnect no longer ends the session: reconnect to the same `wsUrl` or `cdpUrl` until `inactivity_timeout` runs out. Over `wsUrl`: on the first connect create your context and page as usual (`browser.newContext()`, `newPage()`); after a reconnect, `browser.contexts()` lists the contexts you created, with their pages, cookies and page state intact. Over `cdpUrl` (`connectOverCDP`): work in the browser's default context, `browser.contexts()[0]` — contexts created with `newContext()` over CDP close when the client disconnects.

**Use it for:** Agents and step-by-step workflows that disconnect between steps, and code that may lose its connection, without losing the logged-in browser.

Not on Rebrowser. Dedicated threads only (`403 keep_alive_requires_dedicated` on shared plans). The session is billed while it's alive, attached or not — and `browser.close()` only disconnects: the session ends after `inactivity_timeout` without a client, or at once with `DELETE /v1/me/threads/{sessionId}` (or End in the dashboard). Set `inactivity_timeout` to how long it may wait for you.

Example: `true`

 |
| [`start_url`](https://cdpfleet.com/docs/options/start_url) | `string (http/https URL)` | none | 

The server opens the page itself and the session doesn't wait for your code to connect: open a live link in `control` mode (`POST /admin/session/live`) and use it like a local browser. Mouse and keyboard input in control mode count as activity, so the session ends after `inactivity_timeout` without input. Your code can still connect later — `browser.contexts()[0].pages()` holds the page. This is what the dashboard's **Launch interactive** does.

**Use it for:** A person logs in or passes a check by hand in live view first; then your code connects and carries on in the same page.

http or https, at most 2,048 characters (`400 invalid_start_url`). Headless needs `"live_view": true` on a Chromium-based browser; Firefox, Camoufox and WebKit only headful (`400 start_url_not_viewable`). Without `keep_alive`, a client that connects and then disconnects ends the session. See [Launch interactive](https://cdpfleet.com/docs/live-view#launch-interactive).

Example: `"https://example.com"`

 |
| [`profile`](https://cdpfleet.com/docs/options/profile) | `string | object` | none | 

A name (`"shop-login"`, up to 64 letters, digits, `-`, `_`) or `{ "id": "shop-login", "mode": "readonly" }`. The first launch with a new name creates the profile; later launches restore its cookies, localStorage and IndexedDB, and the profile is saved when the session ends. Session cookies are kept too; sessionStorage (per tab) is not. Work in `browser.contexts()[0]` — a context made with `newContext()` is not saved. `"mode": "readonly"` restores without saving and can run in parallel; a second normal session gets `409 profile_in_use`. On Camoufox the device fingerprint is kept with the profile. See [Persistent profiles](https://cdpfleet.com/docs/profiles).

**Use it for:** Staying logged in between sessions: log in once, then every later launch starts with the same cookies and storage (and, on Camoufox, the same device).

Every browser except Whale and Rebrowser. Keeps cookies (session cookies too), localStorage and IndexedDB; not sessionStorage. 20 profiles per shared thread and 30 per dedicated thread (at least 100), 50 MB each; list and delete with `/v1/me/profiles`.

Example: `"shop-login"`

 |
| [`record`](https://cdpfleet.com/docs/options/record) | `boolean` | true (when recording applies) | 

Sessions are recorded by default within your recording limits: headful sessions from their display, headless sessions only when launched with `"cdp": true` or `"live_view": true`. `"record": false` turns it off for one launch.

**Use it for:** Keeping a video of each run for debugging, QA evidence or audits. Set `false` when you don't need it, to keep the recording quota for runs that matter.

Recording applies to headful sessions and to headless ones launched with `"cdp": true` or `"live_view": true`, at 480p (720p or 1080p on a recording plan). Recordings appear on the dashboard's Sessions page. See [Session recording](https://cdpfleet.com/docs/recordings).

Example: `false`

 |

---
Source: https://cdpfleet.com/docs/browsers/chromium · Markdown: https://cdpfleet.com/docs/browsers/chromium.md · All docs for AI tools: https://cdpfleet.com/llms.txt
