chromiumfish
v0.2.8
Published
Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser.
Maintainers
Readme
chromiumfish (Node)
Stealth Chromium with a drop-in Playwright harness.
npm install chromiumfish playwright-core
npx chromiumfish fetch # download + cache the browser buildUsage
import { ChromiumFish } from "chromiumfish";
const browser = await ChromiumFish({ personaSeed: "alpha-7", headless: true });
const page = await browser.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "fp.png" });
await browser.close();ChromiumFish() returns a standard Playwright Browser, so newContext,
newPage, routing, tracing, etc. all work as usual.
Options
| Option | Default | Description |
|--------|---------|-------------|
| personaSeed | — | String id for a stable, internally-consistent fingerprint persona (any string; a number works too). |
| headless | true | Run headless (SwiftShader). |
| proxy | — | Playwright proxy object, e.g. { server, username, password }. |
| windowSize | [1920, 1080] | Window dimensions (null to omit). |
| version | pinned | Override the browser build version. |
| download | true | Auto-download the build if missing. |
| timezone | — | "auto" resolves the egress IP's IANA timezone via the downloadable ip2tz DB and sets the browser's TZ; an IANA string (e.g. "Europe/Berlin") is used verbatim. |
| args | — | Extra Chromium flags. |
| ...rest | — | Forwarded to chromium.launch(). |
IP-to-Timezone
timezone: "auto" aligns the browser clock with the egress IP (handy behind a
proxy). It uses a compact ip2tz database downloaded once and cached; you can
also query it directly:
import { lookupTimezone, resolveTimezone } from "chromiumfish";
await lookupTimezone("8.8.8.8"); // -> "America/Los_Angeles"
await resolveTimezone(); // own egress IP -> timezoneThe DB auto-updates: it tracks the latest monthly build (cached, re-checked
weekly), so you get fresh data without upgrading the SDK. Pin a fixed version
with CHROMIUMFISH_GEOIP_VERSION=2026.06 for reproducibility.
Geolocation (GPS)
Pin a static GPS position with two flags through args. ChromiumFish auto-grants the
Geolocation permission (no prompt) and reports those exact coordinates to every frame,
never querying the real location providers:
const browser = await ChromiumFish({
personaSeed: "alpha-7",
args: ["--persona-lat=48.8584", "--persona-lng=2.2945"], // optional: --persona-accuracy=<m> (default 40)
});Keep the location aligned with the exit IP and timezone. More: chromiumfish.com/personas#geolocation-gps.
Mobile / OS persona
By default the persona is Windows desktop Chrome. --persona-os (through args) switches
the whole OS family — win (default), mac, or android, a seed-driven Pixel-family phone
(mobile UA/hints, touch, phone screen, Mali WebGL):
const browser = await ChromiumFish({ personaSeed: "alpha-7", args: ["--persona-os=android"] });
// navigator.userAgentData.mobile === trueThe Android persona is coherent but not airtight — fonts, exact-match canvas, and locale/timezone still carry desktop tells, so pair it with a mobile-region proxy + timezone and don't rely on it against the strictest detectors. More: chromiumfish.com/personas#mobile-and-os-persona.
AI agent
ChromiumFish ships a native in-browser agent (perceive → think → act, driven by
an OpenAI-compatible LLM). launchAgent starts the browser with the agent layer
and connects over CDP; runTask drives it from a plain-language goal.
import { withAgent } from "chromiumfish";
// LLM config: a nearby .env (OPENAI_API_*), or pass apiKey/apiBase/model here.
const result = await withAgent({ typing: "human" }, (agent) =>
agent.runTask("Search DuckDuckGo for 'chromiumfish' and give me the first result's URL."),
);
console.log(result.finalText);withAgent shuts the browser down for you; use launchAgent directly if you
want to manage the lifecycle (const { agent, close } = await launchAgent()).
| Option | Default | Description |
|--------|---------|-------------|
| typing | "human" | Typing speed: "human" (~75 WPM, natural), "fast", "instant", or a custom [keyDown, keyUp, longMultiplier] triple (numbers = ms). |
| model | env | Model for the session (overrides OPENAI_API_MODEL); runTask({ model }) overrides per task. |
| apiKey / apiBase | env | LLM key / base URL (override OPENAI_API_KEY / OPENAI_API_BASE). |
| chrome | CHROME_BIN / cached build | Path to the ChromiumFish binary. |
| port | 9222 | DevTools remote-debugging port. |
| extraArgs | — | Extra Chromium flags. |
Needs a WebSocket: the Node 22+ global
WebSocketis used automatically; on Node <22 install the optionalwspackage (npm install ws).
External agents
Prefer a third-party framework (Hermes, OpenClaw, browser-use, Playwright, …)? chromiumfish
serve exposes a plain CDP endpoint — with your persona/proxy/timezone active — for any of them
to attach to:
npx chromiumfish serve --persona-seed alice # -> http://127.0.0.1:9222
# e.g. Hermes ~/.hermes/config.yaml: browser: { cdp_url: "http://127.0.0.1:9222" }Or run it as an MCP server for Claude Code/Desktop, Cursor, etc.:
npx chromiumfish mcp --persona-seed alice # exposes browser tools over MCP (stdio)Full guide: chromiumfish.com/agents.
CLI
npx chromiumfish fetch [--browser-version X] [--force] # download + cache
npx chromiumfish path # print binary path
npx chromiumfish serve [--port 9222] [--persona-seed S] # CDP endpoint for external agents
[--proxy URL] [--window-size WxH] [--timezone Z] [--headless]
[--browser-version X] [--extra-args ARGS] [--timeout S]
npx chromiumfish mcp [--persona-seed S] [--headed] # MCP server (Claude, Cursor, ...)
[--proxy URL] [--window-size WxH] [--port N] [--typing T] [--llm-key K]
npx chromiumfish clear # wipe the cache
npx chromiumfish --versionBuilds are cached under ~/.cache/chromiumfish/<version>/ (override with
CHROMIUMFISH_CACHE_DIR). Pin a build with CHROMIUMFISH_VERSION.
Attribution
IP Geolocation by DB-IP — the ip2tz timezone
database is derived from DB-IP City Lite, used under CC BY 4.0.
License
MIT © Arman Hossain. See the repository.
