playwright-secure
Serves as the credential store for browser logins: the login helper reads secrets from a single 1Password vault with the op CLI, authenticated by a read-only, vault-scoped service-account token. Site definitions reference fields as op://<vault>/<item title>/<field>, so logins are performed against the referenced Login items without the credential ever being exposed to the agent.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@playwright-securelog into the staging dashboard and download this month's invoices"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
secure-browser
Browser automation for coding agents where the agent can use an authenticated session but cannot read the credential that created it.
Built for Claude Code and the Playwright MCP server, on Linux. The pattern is not specific to either: a login helper and a browser server run as a separate OS user, the agent talks to the browser over MCP, and the credential never enters the agent's process, files, or transcript.
Why
An agent driving a browser needs to log in. The naive approach, a password in a file the
agent reads, puts the credential into model context, which is transmitted to an API,
summarised, and persisted in transcripts. Moving it to an encrypted store helps, but if the
agent can run the store's show command, the store is a convention rather than a boundary.
Here the credential is owned by a separate OS user. The Playwright MCP server runs as that user, so the agent drives an authenticated browser while the calling user, and therefore the agent's shell, file and search tools, cannot read the vault token, the credentials, or the session cookies.
What this buys: the credential cannot be exfiltrated into a transcript, an API, a log, or anything outliving the session. The server's code-execution tool, which would have been the way around that, is removed by a proxy that runs with the server (see "Tool filtering").
What it does not buy by design: it does not stop the agent doing something undesirable while driving an authenticated browser. That risk is accepted and controlled by the user's approval gates on outward-facing actions.
Related MCP server: agentlock-browser
Tool filtering
The stock @playwright/mcp server includes browser_run_code_unsafe, which runs arbitrary
Node code inside the server process. Here that process is the secrets user, so the tool
could read the service-account token, dump the cookie jar with page.context().cookies(),
or op read the vault. --caps only adds capabilities and neither the command line nor
the config file can remove a core tool (checked against 0.0.83).
So bin/mcp-server does not run the server directly. It runs
mcp-tool-gate, which spawns the server,
strips run_code_unsafe from every tools/list reply, and answers any call to it with a
JSON-RPC error. The gate and the Playwright server are both npm packages pinned in
package.json and package-lock.json; the installer runs npm ci from that lockfile into
the root-owned install directory, so the code the secrets user executes is fixed by content
hash and writable by nobody but root. The launcher never uses npx: an npx cache lives in
the secrets user's home, which that user can write. Refusals are logged to the secrets
user's home. browser_evaluate stays: it runs in the page, not the server process.
A client-side deny (a Claude Code permission rule or the PreToolUse hook) would not do: both live in files the calling user, and therefore the agent, can edit.
The launcher also runs with the exchange directory as its working directory rather than
the secrets user's home. The server treats cwd as the workspace root and
browser_file_upload will attach any file under it, so with $HOME as the root the token
file was uploadable to any site.
History. Both paths were found by the author in a self-audit before the first public commit, not through an incident, and the README of that commit recorded them as an open gap while this fix was built. Remaining optional hardening: run the browser server as a third user that holds the cookie file but not the vault token, so that even a future code-execution tool upstream could not reach the vault.
Layout
secure-browser/
bin/ login helper, MCP server launcher, caller-side CLI
package.json pinned runtime (Playwright MCP server + mcp-tool-gate), installed by the installer
sites.d/ one JSON per site: login URL, expected origin, selectors, secret refs
hooks/ PreToolUse guard that denies the unscoped Playwright server
setup/ privileged installer (run by a human)
claude/ the skill, the CLAUDE.md section and the hook registration
CHANGELOG.md dated historyThree enforcement layers
Layer | Where | Strength |
Skill |
| procedures; fires on description match |
Instruction |
| always loaded |
Hook |
| deterministic: denies any Playwright MCP call under the root that is not on the |
The hook has three modes in hooks/mode (missing file means warn):
warn: logs violations tohooks/guard.log, allows the call. Correct until the secure server is actually running.enforce: denies any Playwright call under the root that is not on theplaywright-secureserver.off.
Leave it on warn until setup/install.sh has run and a login works end to end.
Switching to enforce first makes all browser automation fail.
Credential store
1Password, read with the op CLI as the secrets user, using a service account whose
token lives at ~<secrets user>/.config/op/service-token (mode 600, directory 700). The
calling user cannot read the token, and the helper never prints it or passes it in argv; it
goes into the child environment of op read and nowhere else.
The token is the root secret: unattended by design, protected by the OS user boundary. Two consequences shape the setup:
Scope it to one vault. A service-account token is cloud-scoped: whoever holds it can read every vault it was granted, from anywhere. So the service account gets read-only access to a single vault, which holds only the credentials automations are allowed to read. Private and shared vaults are never granted.
Login needs the network.
op readcalls 1password.com. An outage shows up asFAILED <site>: 1Password unreachable.
Site files reference fields as op://<vault>/<item title>/<field>. The user manages the
items in the 1Password apps like any other login; the same item serves both the user and
the helper, so a password rotation is one edit. Legacy pass entries (no op:// prefix)
still resolve through a local pass store if one exists.
Setup, done by the user in the 1Password web app: create the vault, create the Login items
(default username/password field names), create a service account under Developer
with read access to that vault only, and paste its token into the installer's hidden
prompt. 1Password shows the token once; keep a copy in your own Private vault.
Install
Prerequisite: a system-wide Node 20+. The MCP server runs as the secrets user under
sudo, which resets PATH to secure_path, so a user-level nvm Node is invisible to it.
Debian and Ubuntu's apt nodejs is too old for @playwright/mcp. The installer checks and
stops if it is missing.
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs
/usr/bin/node -v # must print v20 or higherThe installer also needs python3-venv for the secrets user's Playwright venv and installs
it itself.
sudo bash setup/install.sh # user, group, exchange dir, sudoers rule, /opt copy + npm ci, op CLI, token
newgrp browseragent # group membership does NOT apply to already-open shells
secure-browser status # should show everything presentThe install step runs npm ci inside the install directory from the committed
package-lock.json, so it needs the npm registry once. Upgrading the server or the gate is
a package.json bump, npm install in the repo to refresh the lockfile, commit, re-run
the installer.
Names and paths are overridable with SECURE_BROWSER_AGENT_USER, SECURE_BROWSER_GROUP,
SECURE_BROWSER_INSTALL and SECURE_BROWSER_EXCHANGE, set for the installer and for the
caller-side CLI alike. Defaults: pwagent, browseragent, /opt/secure-browser,
/srv/secure-browser-exchange.
Then add a site: copy sites.d/EXAMPLE-ats.json.template to sites.d/<site>.json, fill it
in, create the matching Login item in the vault, and re-run the installer. Real site files
are gitignored on purpose; see sites.d/README.md.
Registering the server
Add to ~/.claude.json (user scope) or the project's .mcp.json:
"mcpServers": {
"playwright-secure": {
"command": "sudo",
"args": ["-n", "-u", "pwagent", "/opt/secure-browser/bin/mcp-server"]
}
}The name must stay playwright-secure: the guard hook recognises the hardened server
by that string in the tool name. Register the hook with the block in
claude/settings.hooks.json, and paste claude/CLAUDE.md.snippet into the project's
CLAUDE.md. Copy claude/skills/secure-browser/ into the project's .claude/skills/.
How a login reaches the MCP server
login signs in with Playwright-Python and saves the cookies to
~<secrets user>/.browser-profile/storage-state.json (one file for all sites, mode 600). The
MCP server runs --isolated and seeds its context from that file at startup only. So:
secure-browser login <site>→OK ... session savedstart a new agent session; the server is spawned at startup and reads the file then
A login done while a session is open does not reach the already-running server. Cookies the
agent picks up during a session are in memory only and are discarded at exit; the helper's
file is the source of truth. login is safe to run at any time: when the saved cookies are
still valid it reports existing session reused without touching the credential.
Everyday use
secure-browser status # what is installed and what is not
secure-browser login <site> # OK / FAILED / NEEDS-HUMAN, one line
secure-browser login <site> --check-secrets # prove the store path; nothing is typed
secure-browser put ./upload.pdf # stage a file for upload, prints the path
secure-browser ls # what is staged
secure-browser clean upload.pdf # remove it againWhy the registry is not editable by your normal user
sites.d/*.json says where a credential gets typed. A site file therefore carries the
same authority as the credential itself: anyone who can edit one can point a password at a
host they control. So the registry is installed to root-owned /opt alongside the
executables, and adding a site means re-running the installer.
That is real friction, and it is the price of the guarantee. The same reasoning applies to
bin/login: if your user could edit the script that runs as the secrets user, the boundary
would be decorative.
Error taxonomy
login prints exactly one line and nothing else.
Output | Meaning |
| cookies still valid; no credential was touched |
| credential used, session persisted |
| captcha, MFA or an interstitial. Stop and tell the user; do not attempt it |
| the page was not where it should be. Nothing was typed. Treat as hostile, do not retry blind |
| vault, item title or field name does not match 1Password, or the service account cannot read that vault |
| token revoked, mistyped, or not granted this vault; create a new one and re-run the installer |
| network or DNS from this machine; retry later |
| installer has not stored a token yet |
| environment, not security |
| unexpected. Detail goes to |
A traceback is never printed, because it can carry page fragments or the value. The exception type is printed, because an opaque failure is its own kind of risk.
Status
See CHANGELOG.md. The design has been in daily use by the author since 2026-10-11.
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.
Scoped agent execution. Server-side credentials, policy, budgets and verifiable receipts.
Stealth web browser for agents: search, fetch, click, download and type in persistent MCP sessions.
Cloud browsers for AI agents: get past bot detection, sign in with MFA, run web tasks at scale.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenancePolicy-gated MCP server for local-first browser automation, enabling agents to drive isolated Chromium sessions via snapshots, revision-bound refs, typed actuation, scripts, and telemetry receipts.Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables an agent to drive a real browser while enforcing provenance-based gating so URLs and form values must trace to the user or an allowlist, never to untrusted page content.AGPL 3.0
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to drive a real, already logged-in browser from the CLI or over MCP, with enforceable approval gates for actions and safeguards against prompt injection.1,969,715 npmMozilla Public 2.0
- AlicenseNot gradedqualityBmaintenanceEnables policy-gated browser automation with persistent Firefox/Chromium profile isolation, version-locked fingerprint consistency, and auditable local session management.Apache 2.0