Drive a remote chrome-devtools-mcp server (typically on a tailnet) over HTTPS using the chrome-devtools CLI. Use this when the user wants to navigate, screen...
---
name: chrome-devtools-remote
description: Drive a remote chrome-devtools-mcp server (typically on a tailnet) over HTTPS using the chrome-devtools CLI. Use this when the user wants to navigate, screenshot, inspect, or evaluate JavaScript on a browser running on another host (e.g. a Tailscale-connected Mac mini or a CI runner) — and you don't have a local Chrome to control. Examples of triggers ("open <url> on the lab mac", "take a screenshot of the browser on host X", "evaluate this on the remote browser").
---
# chrome-devtools-remote
You are operating the `chrome-devtools` CLI in **remote** mode against a `chrome-devtools-mcp` server that the user has running on another host. The remote server owns the browser; your job is to drive it.
## When to use this skill
Trigger on any request that:
- Names a remote machine ("on the lab mac", "on host X", "on my tailnet box") and asks you to do something a browser would do
- Provides a `chrome-devtools-mcp` HTTPS URL (typically ending in `/mcp`) and asks for an interactive browser action
- Mentions environment variables `CHROME_DEVTOOLS_MCP_REMOTE_URL` or `CHROME_DEVTOOLS_MCP_REMOTE_INSECURE`
If the user wants to drive a **local** browser, use the regular `chrome-devtools` CLI (without `--remote`) or the `mcp__chrome-devtools__*` MCP tools instead.
## Install (on the local box, once)
The `chrome-devtools` client CLI ships in the same npm package as the server. Install globally on the machine where the agent runs — **not** on the remote host:
```bash
npm install -g @vibebrowser/chrome-devtools-mcp
chrome-devtools --version # should print 0.26.6 or newer
```
You do not install anything on the remote host — that host is whoever already serves `https://.../mcp` and is set up out-of-band.
Troubleshooting:
- `command not found: chrome-devtools` — npm's global bin isn't on `PATH`. Add `$(npm config get prefix)/bin` to `PATH`, or on macOS Homebrew add `eval "$(brew shellenv)"` to your shell rc.
- `Cannot find package 'pkce-challenge'` on first run — known bundling gap (issue dzianisv/chrome-devtools-mcp#17). Workaround: `cd "$(npm root -g)/@vibebrowser/chrome-devtools-mcp" && npm install pkce-challenge --no-save`.
## Connect
Configure the endpoint **once per shell session** and verify connectivity before doing anything else.
```bash
export CHROME_DEVTOOLS_MCP_REMOTE_URL="https://lab.tailnet.ts.net/mcp"
chrome-devtools status --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
```
A healthy response prints `status=ok http=200` plus a JSON body. Anything else is a hard stop — surface the error to the user instead of retrying.
If the URL is unset, ask the user once for the endpoint. Conventional shape: `https://<host>/mcp` (the `/mcp` path is required — the bare host returns a 404).
Connection-time flags:
| Situation | What to pass |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Self-signed cert (common on tailnets without Tailscale-issued certs) | `--insecure` on every call, or `export CHROME_DEVTOOLS_MCP_REMOTE_INSECURE=1` |
| Bearer-token gateway | `--header "Authorization: Bearer $TOKEN"` — repeatable, **not** cached, must be on every invocation |
| Custom static header (e.g. `X-Tenant: foo`) | `--header "X-Tenant: foo"` |
| Endpoint behind Tailscale and `status` returns `Failed to reach remote` | `tailscale status` locally; the box is offline or the URL has the wrong hostname |
Once `status` is green, every subsequent `chrome-devtools <tool> ... --remote "$URL"` call reuses the same server-side tab via a sticky session id cached at `~/.cache/chrome-devtools-mcp/remote/<hash>.session`.
## Session model — read this before chaining commands
Each CLI invocation reuses a sticky session id stored under `~/.cache/chrome-devtools-mcp/remote/<hash>.session`. This means:
- `navigate_page` → `take_snapshot` → `click` → `take_screenshot` all hit the **same** server-side tab. You can chain them as separate CLI calls and assume continuity.
- If the server restarts between calls, the next call transparently re-initializes against a fresh session. **Tab state on the server is lost** — re-navigate before assuming anything.
- To explicitly end the server-side session and free its browser context, run `chrome-devtools stop --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"`.
## Common operations
All commands accept `--output-format json` to return structured output you can pipe into `jq`.
```bash
# Open a page
chrome-devtools navigate_page "https://example.com" --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
# Get a structured a11y snapshot (use for finding clickable uids)
chrome-devtools take_snapshot --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL" --output-format json
# Click an element by uid from the snapshot
chrome-devtools click "$UID" --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
# Fill an input
chrome-devtools fill "$UID" "value" --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
# Take a screenshot (saved to /tmp/<uuid>.png locally)
chrome-devtools take_screenshot --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
# Evaluate JS in the page — return value must be JSON-serializable
chrome-devtools evaluate_script '() => document.title' --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
# Read console messages from the page
chrome-devtools list_console_messages --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
# List network requests since page load
chrome-devtools list_network_requests --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
```
## Recipes
### Smoke-check a deployed web app
```bash
URL="$CHROME_DEVTOOLS_MCP_REMOTE_URL"
chrome-devtools navigate_page "https://app.example.com" --remote "$URL"
chrome-devtools evaluate_script '() => ({title: document.title, ready: document.readyState})' --remote "$URL"
chrome-devtools list_console_messages --remote "$URL" --output-format json
chrome-devtools take_screenshot --remote "$URL"
```
If `console_messages` contains any `level: "error"` entries, surface them — they are usually the root cause of "the page looks broken" reports.
### Drive a login form
```bash
chrome-devtools navigate_page "https://app.example.com/login" --remote "$URL"
chrome-devtools take_snapshot --remote "$URL" --output-format json > /tmp/snap.json
# Find the uid of the email input + password input + submit button from /tmp/snap.json
chrome-devtools fill "<email-uid>" "$LOGIN_EMAIL" --remote "$URL"
chrome-devtools fill "<password-uid>" "$LOGIN_PASSWORD" --remote "$URL"
chrome-devtools click "<submit-uid>" --remote "$URL"
```
### Capture a CrUX-style trace
```bash
chrome-devtools performance_start_trace --remote "$URL"
chrome-devtools navigate_page "https://app.example.com" --remote "$URL"
chrome-devtools performance_stop_trace --remote "$URL" --output-format json
```
## Output discipline
- Always print the URL of the page you navigated to so the user knows which tab they're looking at.
- For screenshots, print the path of the saved file (`Saved to /tmp/<uuid>.png.`) — the CLI already writes that line on stdout.
- For `evaluate_script`, prefer `--output-format json` and forward only the relevant field instead of dumping the whole structuredContent envelope.
- If the user asks "what's on the page", prefer `take_snapshot` over `take_screenshot` — the snapshot is text and cheaper to reason over.
## Failure modes
| Symptom | Cause | Fix |
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `Failed to reach remote` | DNS, Tailscale offline, or wrong URL | `tailscale status` on the local box; verify `--remote` URL |
| `Streamable HTTP error: ... 404 Session not found` | Server restarted; sticky id was stale. Retried once internally — if you see this it means the retry also 404'd | `chrome-devtools stop --remote $URL` to wipe the sticky pointer, then retry the command |
| TLS verify error (`UNABLE_TO_VERIFY_LEAF_SIGNATURE`, `SELF_SIGNED_CERT_IN_CHAIN`) | Server uses a self-signed cert (no Tailscale-issued cert) | Add `--insecure` or `export CHROME_DEVTOOLS_MCP_REMOTE_INSECURE=1` |
| `Bad Request: Mcp-Session-Id header is required` | Local cache directory was wiped mid-session | First call after the wipe will mint a fresh session; just retry |
| Hangs on first call after a long idle | Server idle reaper closed the session | Same fix as the 404 row — `stop` then retry |
## Don'ts
- Don't loop `navigate_page` to "wait for the page to load" — use `wait_for` with a text selector instead.
- Don't `take_screenshot` for every step in a chain — the user almost always wants one final screenshot, not five intermediate ones.
- Don't pass `--insecure` for a hosted endpoint with a real cert. It silently disables TLS for the whole CLI process.
- Don't try to `start` a server with `--remote` — that subcommand only manages local daemons.
don't have the plugin yet? install it then click "run inline in claude" again.
extracted and formalized 6 implexa components (intent, inputs with auth/transport details, procedure with step-by-step actions and io, decision points with all failure modes and conditionals, output contract with format specs, outcome signal with success criteria) while preserving original procedure, recipes, and failure mode table as reference material.
this skill drives a chrome-devtools-mcp server running on a remote host (e.g. a tailnet mac mini, ci runner, or staging box) over HTTPS. use it when the user asks for browser actions on a named remote machine or provides an mcp endpoint URL and you don't have a local browser to control. triggers include "open this url on the lab mac", "take a screenshot of the remote browser", or "evaluate this javascript on host X".
cli tool (local installation)
chrome-devtools CLI (v0.26.6+) installed globally via npm install -g @vibebrowser/chrome-devtools-mcp on the agent's machine only, not on the remote host.chrome-devtools --version.remote server endpoint (required)
CHROME_DEVTOOLS_MCP_REMOTE_URL: full HTTPS URL of the remote mcp server, typically https://<host>/mcp. ask the user once if unset./mcp path suffix is mandatory. the bare host returns 404.authentication / transport (optional, conditional)
CHROME_DEVTOOLS_MCP_REMOTE_INSECURE=1 or --insecure flag: required if the remote server uses a self-signed TLS cert (common on tailnets without tailscale-issued certs). do not pass this flag for endpoints with valid certs.--header "Authorization: Bearer $TOKEN" (repeatable): for bearer-token gateways. not cached; must be passed on every invocation.--header "X-Custom-Header: value" (repeatable): for custom static headers (e.g. tenant routing). not cached; must be passed on every invocation.session state (automatic)
~/.cache/chrome-devtools-mcp/remote/<hash>.session: sticky session id cached by the cli. persists across separate invocations of the same endpoint. automatically wiped on chrome-devtools stop.network / connectivity (implicit)
tailscale status.input: none (first time setup only).
action:
npm install -g @vibebrowser/chrome-devtools-mcp
troubleshooting edge cases:
command not found: chrome-devtools, npm's global bin is not on PATH. run export PATH="$(npm config get prefix)/bin:$PATH" and retry.Cannot find package 'pkce-challenge' on first run, run:cd "$(npm root -g)/@vibebrowser/chrome-devtools-mcp" && npm install pkce-challenge --no-save
output: chrome-devtools CLI is callable from any shell session.
input: either CHROME_DEVTOOLS_MCP_REMOTE_URL env var, or ask the user for the URL.
action:
export CHROME_DEVTOOLS_MCP_REMOTE_URL="https://lab.tailnet.ts.net/mcp"
chrome-devtools status --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
expected output: status=ok http=200 followed by a json body containing server metadata.
output: confirmed remote endpoint URL in $CHROME_DEVTOOLS_MCP_REMOTE_URL.
input: valid remote endpoint URL.
action: the first chrome-devtools <tool> command against the endpoint will mint a new session id and cache it at ~/.cache/chrome-devtools-mcp/remote/<hash>.session.
output: sticky session id stored locally. all subsequent commands reuse the same server-side browser tab.
input: endpoint URL, optional auth headers, optional --insecure flag, command name, command arguments.
action: run any of the following as separate cli invocations. each command reuses the sticky session from step 3.
navigate to a url:
chrome-devtools navigate_page "https://example.com" --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
output: confirmation of navigation (url printed).
take a text/a11y snapshot (for finding uids to click):
chrome-devtools take_snapshot --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL" --output-format json
output: json object with accessibility tree, uids for each clickable element.
click an element by uid:
chrome-devtools click "$UID" --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
output: confirmation of click.
fill an input field:
chrome-devtools fill "$UID" "value" --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
output: confirmation of text entry.
take a screenshot:
chrome-devtools take_screenshot --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
output: path to saved png file (e.g. Saved to /tmp/<uuid>.png).
evaluate javascript in the page:
chrome-devtools evaluate_script '() => document.title' --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL" --output-format json
output: json object with the return value (must be json-serializable on the server side).
read console messages:
chrome-devtools list_console_messages --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL" --output-format json
output: json array of console log/warn/error entries with level and timestamp.
list network requests since page load:
chrome-devtools list_network_requests --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL" --output-format json
output: json array of http requests with url, status, duration.
start and stop performance tracing:
chrome-devtools performance_start_trace --remote "$URL"
chrome-devtools navigate_page "https://example.com" --remote "$URL"
chrome-devtools performance_stop_trace --remote "$URL" --output-format json
output: trace json (crux/devtools format).
input: endpoint URL.
action:
chrome-devtools stop --remote "$CHROME_DEVTOOLS_MCP_REMOTE_URL"
output: session cache wiped. next command will mint a fresh session.
if the user names a remote machine but provides no url: ask them for the mcp endpoint url (conventional format: https://<hostname>/mcp). if they don't know, ask them to verify the remote host is running a chrome-devtools-mcp server and get the endpoint from whoever set it up.
if chrome-devtools status returns Failed to reach remote: this is a hard stop. don't retry the status check. diagnose first: run tailscale status on the local machine to confirm the tailnet is connected, and verify the url is spelled correctly (especially the hostname). surface the diagnostic output to the user instead of guessing.
if the remote server uses a self-signed TLS cert (typical for tailnet hosts without tailscale-issued certs): pass --insecure on every cli invocation, or set export CHROME_DEVTOOLS_MCP_REMOTE_INSECURE=1 once per session. do not pass --insecure to hosted endpoints with valid certs, as it disables tls verification globally.
if the endpoint requires bearer-token auth (e.g. a gateway in front of the mcp server): pass --header "Authorization: Bearer $TOKEN" on every invocation. this header is not cached and must be repeated for each command.
if the endpoint requires custom headers (e.g. X-Tenant: foo for multi-tenant routers): pass --header "X-Tenant: foo" on every invocation. not cached.
if list_console_messages returns entries with level: "error": surface these to the user immediately. they are usually the root cause of "page looks broken" reports and should be investigated before proceeding.
if a command returns 404 Session not found: the server restarted and the sticky session id became stale. the cli retries once internally, but if you see this error, the retry also failed. run chrome-devtools stop --remote "$URL" to wipe the local cache, then retry the command (a fresh session will be minted).
if a command hangs after a long idle period: the server's idle reaper likely closed the session. same fix as the 404 row: stop then retry.
if the cli reports Mcp-Session-Id header is required: the local cache directory was wiped mid-session. the next invocation will mint a fresh session automatically, so just retry.
if the user asks "what's on the page": prefer take_snapshot over take_screenshot. snapshot is text-based and faster to reason over; screenshot is a binary image and heavier to process.
if the user asks you to wait for something to appear on the page: do not loop navigate_page as a polling mechanism. instead, use wait_for with a text selector to block until the element is visible.
navigation: print the url navigated to so the user knows which tab they're looking at.
screenshots: the cli prints the file path automatically (e.g. Saved to /tmp/<uuid>.png). forward that path to the user.
snapshots: return structured json via --output-format json. forward the entire snapshot object or extract the relevant accessibility tree branch.
evaluate_script: with --output-format json, return only the relevant field from the result (not the whole structuredContent envelope). if the script returns an object, forward the object; if a string, forward the string.
console_messages: return the json array as-is, or filter to only level: "error" entries if the user is debugging.
network_requests: return the json array of request objects (url, status, duration, initiator, etc).
performance traces: return the raw crux/devtools json trace.
all errors: if a command fails, surface the exact cli error message to the user (e.g. Failed to reach remote, 404 Session not found). don't hide stderr.
the user knows the skill worked when:
chrome-devtools status returns status=ok http=200 and the user sees no error messages.Saved to /tmp/<uuid>.png and the user can open the image.evaluate_script matches what the user expected (e.g. title: "Example Domain").list_console_messages exposes them and the user can act on the root cause.