Security
Deny-all by default. Domain allowlist, mutation and eval gates, redaction, the audit log, and the pairing token.
Deny-all safe mode. With no policy configured: empty domain allowlist,
eval off, downloads off, mutating tools off. Opt in explicitly:
chrome-mcp --allow-domain example.com --enable-mutations
chrome-mcp --policy ./policy.json # see policy.example.json
chrome-mcp --unsafe-all-domains # loud footgun
chrome-mcp --enable-observers # console/network/dialog capture (patches page globals)
chrome-mcp --redact # scrub secret-shaped strings out of page readsAllowing a site without a restart. When a call is refused because its site
is not allowed, the extension's toolbar icon shows a ?. Click it: the popup
lists the blocked site with an Allow button, shows whether the site you are on
is allowed (with its own Allow), and lists the allowed sites. The Options page
(Settings in the popup) also has a box to allow any other site. An allowed site applies at once to every session sharing that Chrome, and
is saved to ~/.chrome-mcp/allowed-sites.json so it survives restarts. Sites
allowed there can be removed there; sites from --allow-domain stay until the
flags change. The catch-all * can only come from --unsafe-all-domains. The
agent cannot approve a site itself: MCP Browser Extension never reads or drives the
extension's own pages. Run with --no-site-grants to keep the allowlist exactly
what the flags say.
What comes back is gated too. The allowlist decides which pages may be read; it says nothing about what is on them. A logged-in page routinely renders a session token into a script tag or an API key onto a settings screen.
- Password field values are always suppressed — in
get_html, and insnapshot, where the field still appears (so you can type into it) flaggedsecret: truewith no value. No flag, no opt-in: nobody wants those characters. --redactadditionally scrubs secret-shaped strings — JWTs, AWS/GitHub/Slack/ Google keys,Bearerheaders, private-key blocks — out ofget_text,get_html,read_as_markdownandeval. It is opt-in because a pattern will eventually fire on something you actually wanted.--redact-pattern <regex>adds your own (and implies--redact); an invalid one fails at startup rather than silently never matching.- Redaction runs before the output cap, so a truncated read cannot leak what a full one would have hidden.
Every call is recorded to the task's history.jsonl with the URL it touched, the
policy verdict (allowed/denied), how long it took, how many bytes came back,
and how many secrets were scrubbed — so "what did the agent do in my browser" has
an answer after the fact.
The per-boot 256-bit token in ~/.chrome-mcp/handshake.json (mode 0600) is the
only trust boundary; it is never written to stdout/stderr. On POSIX the mode is
re-verified after every write and the server fails closed if the file ends up
group/other-accessible. Windows has no such bits — chmod there only toggles the
read-only attribute — so the check is skipped and the token's confidentiality
rests on the per-user ACL of %USERPROFILE%\.chrome-mcp.
Updates
You don't have to do anything to stay current. The setup snippets above use
@latest, and on top of that an installed copy (npx or a global install) checks
npm when it starts: if a newer version is published, it starts that version in
its place, with the same arguments, so an MCP config written months ago still
runs the newest release. The check gives up after 1.5 s when you are offline,
and a copy run from a git checkout never updates itself. The new server also
refreshes the unpacked extension in ~/chrome-mcp-extension, which reloads
itself; the Chrome Web Store copy updates through Chrome as usual. Turn it off
with --no-auto-update or CHROME_MCP_AUTO_UPDATE=0.
Telemetry
The MCP Browser Extension server (not the extension) sends anonymous usage statistics to PostHog, so the project can see how many installs are active, which versions and platforms are in use, and which tools fail most. A notice is printed the first time it runs.
What is sent: a random install id (kept in ~/.chrome-mcp/telemetry.json), the
server version, OS, CPU architecture and Node major version, whether the
session owns the bridge port or shares it, how many browsers are paired, the
paired extension's version and whether it is the store or an unpacked copy, and
per-tool call and error counts with error codes. The first batch goes out a
minute after the first tool call, then every 10 minutes. For PostHog's MCP
Analytics view, each tool call is also sent as one $mcp_tool_call event
carrying only the tool name and its group (reading, navigation...), its
duration, whether it failed and the error code, the MCP client's name and
version (such as claude-code), the MCP protocol revision, the AI model when the
client names it in its own request metadata (Codex does), and a random session
id that changes after 30 idle minutes. When a client connects, one event records
that handshake and the names of the tools offered. Nothing an agent sends or
reads: no arguments, results, error messages or intents. Batched every 15
seconds. One pair_check event
a minute after startup says whether a browser paired and, if not, which of a few
fixed reasons applies (no_extension, token_mismatch, version_mismatch,
profile_mismatch), so setup problems can be fixed where they happen.
What is never sent: URLs, domains, tool arguments, page content, screenshots, cookies, profile names, tokens, file paths, or anything you type. Events are personless and GeoIP lookup is disabled.
The browser extension sends nothing — it only ever talks to 127.0.0.1.
Turn it off with any of:
CHROME_MCP_TELEMETRY=0 # or false / off
DO_NOT_TRACK=1
--no-telemetry # server flagLearn more
The full documentation, setup guides for each MCP client and the security model live on the website: MCP Browser Extension docs.
- What an MCP browser extension is, and when you need one
- Use Claude in your signed-in Chrome
- Give Claude Code browser access
- Give Cursor a real browser with MCP
- How it compares with other browser MCP servers
Policy file
Pass --policy ./policy.json to configure everything in one place. The example shipped with the package:
{
"allowDomains": ["example.com", "*.wikipedia.org"],
"allowEval": false,
"allowDownloads": false,
"allowAllTabs": false,
"enableMutations": true
}