surf-cli: a Unix socket bridge between AI agents and your own Chrome
The CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.
At a glance
- What is it?
- Surf is a Node CLI that drives Chrome through a browser extension and a native messaging host, so any agent that can run shell commands can click, read and screenshot a page. The design is sound for local, agent-agnostic automation; the install ceremony and the trust model of a remote credential are the parts to inspect before adopting.
- Who is it for?
- Adopt surf-cli if your agent already runs shell commands and you want Chrome control without an MCP server, a relay process or a subscription, and if you are willing to load an unpacked extension and run surf install once per browser. Do not adopt it if you need a hosted, multi-tenant browser service, or if nobody on the team can debug a native messaging manifest.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap Surf fills: agent-hostile pages without an MCP server
Most browser automation stacks assume the agent is a Python script that can import Playwright, or that the model vendor ships the browser. Surf assumes neither. The README frames the problem as setup cost and provider lock-in: tools that "require complex setup, tie you to specific AI providers, or break on real-world pages." The stated audience is anything that can run shell commands, named in the README as Claude Code, GPT, Gemini, Cursor, custom agents and shell scripts. The interface is a CLI over a Unix socket, so the integration surface is a process boundary rather than a library import. Two claims in the README are worth separating. The first is architectural: agent-agnostic because there is no SDK to bind to. The second is empirical: "battle-tested", described as reverse-engineering production browser extensions and working through pages the README calls agent-hostile, with Discord settings given as the example. The architectural claim is verifiable from the install steps. The second is a maintainer statement, not something this review can confirm.
Extension, native host, Unix socket: the actual data path
Surf is three pieces. A Chrome extension holds the browser-side privileges. A native messaging host, a Node file the README refers to as host.cjs, runs outside the browser and speaks to the extension over Chrome's native messaging channel. The CLI talks to that host over a Unix socket. Commands in the README follow that path: surf go "https://example.com" navigates, surf read returns page content, surf click e5 acts on an element reference, surf snap takes a screenshot. The e5 style reference implies the extension assigns stable element handles that the agent passes back on the next call, which keeps the model from re-deriving selectors. Two defaults are stated explicitly. Screenshots auto-resize to 1200px, which the README justifies as saving tokens, and actions auto-capture a screenshot, which it justifies as saving round-trips. Both trade fidelity for context budget. Errors on restricted pages warn instead of fail, so a script that only checks exit codes can continue past a page the extension cannot touch. There is also a network capture layer that logs requests while active, with filtering, search and replay of API calls, which avoids writing your own request interception.
Getting it running: four steps and a path you have to paste
The quick start is four commands plus two manual steps. Install with npm install -g surf-cli. Open chrome://extensions, enable Developer mode, click Load unpacked, and paste the path printed by surf extension-path. Copy the extension ID from chrome://extensions and run surf install <extension-id>. Restart Chrome, then test with surf tab.list. Multi-browser installs take a flag: surf install <extension-id> --browser brave, with chrome, chromium, brave, edge, arc and helium listed as supported, and --browser all for every one. WSL2 gets special handling. Running surf install inside WSL2 detects the environment and writes a Windows-side native messaging manifest for Windows Chrome, Brave or Edge by default, with a generated wrapper that launches the WSL2 host through wsl.exe. If you use a Linux browser under WSLg instead, the documented form is surf install <extension-id> --target linux. Package manager installs that put binaries in non-standard locations need SURF_NODE_PATH, SURF_HOST_PATH and SURF_EXTENSION_PATH exported before surf install. Removal is surf uninstall, with --all for every browser plus wrapper files and --target linux for the WSLg configuration.
Remote mode over Tailscale, and what a credential actually grants
Remote Surf splits the CLI and the browser across two Tailnet machines. On the browser host you authorize a named client first: surf remote authorize agent-macbook --output ~/agent-macbook.surf-credential.json, then surf remote list, then surf install <extension-id> --listen 100.101.102.103:4321. The credential file is created mode-0600 and contains the client's Ed25519 private identity plus the pinned host identity; the host keeps only the client's public identity in ~/.surf/remote/remote-clients.json. The client side uses surf --remote 100.101.102.103:4321 --remote-credential ~/.config/surf/agent-macbook.json tab.list, or the SURF_REMOTE and SURF_REMOTE_CREDENTIAL environment equivalents. The README is explicit that Tailnet reachability is not authorization and that mutual Ed25519 challenge-response with fresh nonces runs throughout the connection. The sentence to read twice is this one: a credential grants the same browser and host-file authority as a trusted local Surf user. That is a large grant, and the README's own mitigations are procedural, giving each client its own credential and revoking with surf remote revoke agent-macbook if a client or file is lost. The listener exists only while the extension's native messaging connection is alive, and --listen persists the Tailnet address in the native-host wrapper until you re-run surf install without it.
Scheduling, cancellation and side effects that are not rolled back
Local and remote requests share one host scheduler. The README describes each tab as having a FIFO lane, different tabs executing concurrently, and browser-wide writers held exclusively. That is a reasonable compromise for a tool where two agents may act on the same browser: per-tab ordering prevents interleaved clicks inside one page, while cross-tab parallelism keeps unrelated work moving. The failure semantics are stated plainly and are the part most likely to surprise a caller. Disconnects and timeouts abort queued or in-flight work and retain admission until request-owned cleanup drains or a hard deadline is reached, and browser side effects that already completed are not rolled back. In practice a timed-out surf click may still have clicked. An agent that retries on timeout without checking page state can double-submit a form. The README does not describe an idempotency key or a transaction log, so retry safety is the caller's problem. Precedence rules are documented: --remote beats SURF_REMOTE, --remote-credential beats SURF_REMOTE_CREDENTIAL, and a selected remote endpoint overrides SURF_SOCKET and the default local socket.
Where Surf is the wrong tool
Surf is a local-first tool with a manual install step per browser, and that shapes its limits. It is not a hosted browser service: there is no cloud execution, so a CI runner that has no Chrome profile, no display and no ability to load an unpacked extension cannot use it without building that environment first. It is not a headless scraping library either. The extension and native host live inside a real browser session, which is the source of its advantage on logged-in pages and the reason it cannot scale to hundreds of isolated sessions the way a process-per-context framework can. The AI-without-API-keys feature, which queries ChatGPT, Gemini, Perplexity and Grok through existing browser logins, depends on those sessions staying valid and on those vendors not changing their pages; the README's own framing of agent-hostile pages is an admission that page structure drives reliability. Restricted pages warn instead of fail, so a pipeline that treats non-zero exit as the only error signal will silently proceed on a page where nothing happened. And the native messaging layer is the classic failure point: the README already documents the "Access to the specified native messaging host is forbidden" error and its fix, which tells you how often that class of problem appears.
How Surf differs from Playwright and from MCP-based browser tools
Playwright is the obvious alternative and the difference is not features, it is where the browser lives. Playwright launches and owns browser processes from your code, gives you a typed API in the same process, and is designed for headless CI with reproducible contexts. Surf attaches to the Chrome you already have open, exposes commands over a socket, and assumes a human-installed extension. That makes Surf better when the value is the logged-in session and the agent is a shell-running LLM, and worse when you need determinism, parallel isolated contexts, or a run that reproduces on a clean machine. The README's comparison table positions Surf against MCP-based options such as DevTools MCP, which it marks as needing MCP setup, and against vendor-locked extensions from Manus and Claude. The real distinction is configuration surface: an MCP server is a process your agent host must be told about and keep alive, whereas Surf is a binary on PATH plus a socket. That is a genuine reduction in moving parts. It is also a reduction in structure: there is no schema negotiation, no capability discovery, and no standard error taxonomy, so the agent has to learn the command set from documentation.
Licence, maintenance and what to check before you commit
Surf is MIT licensed, which permits commercial and private use, modification and redistribution provided the copyright notice and permission notice are retained. That is a permissive baseline and this is not legal advice; if you redistribute a modified host.cjs or bundle the extension, have counsel confirm the notice requirements. Maintenance signals visible here are a steady release cadence, with v2.18.0 on 2026-09-05, v2.17.0 on 2026-08-28 and v2.16.1 on 2026-08-23, and a last push on 2026-09-10. The README points to CHANGELOG.md for current release notes, which is where upgrade risk will be documented rather than here. Upgrade cost is mostly re-running surf install when the native host path or manifest changes, since the extension and host are versioned together; the SURF_NODE_PATH, SURF_HOST_PATH and SURF_EXTENSION_PATH variables exist precisely because package manager layouts move binaries around. The concrete pre-adoption check is small: confirm surf extension-path prints a directory Chrome will accept, confirm surf install <extension-id> writes the manifest for your browser and OS, and confirm surf tab.list survives a Chrome restart. If those three pass, the rest of the surface is ordinary CLI behaviour. If the third fails, the problem is almost always the extension ID or the manifest, and the README's forbidden-host error is the message you will see.
Editorial conclusion
Adopt surf-cli if your agent already runs shell commands and you want Chrome control without an MCP server, a relay process or a subscription, and if you are willing to load an unpacked extension and run surf install once per browser. Do not adopt it if you need a hosted, multi-tenant browser service, or if nobody on the team can debug a native messaging manifest. Verify first that surf extension-path and surf install <extension-id> complete on your target OS, that surf tab.list returns after a Chrome restart, and that your policy allows an extension holding the same browser and host-file authority as a trusted local user.
Community notes