Chrome DevTools for Agents: Agent Plugins 1.0 and the Flag That Turns JavaScript Off

·11 min read·Evergreen Tools Team

Chrome moved to two releases a month, which is why the What's new in DevTools series is now a monthly recap instead of one post per version. This edition covers Chrome 153 and Chrome 154 landing in stable, and it was published on September 22, 2026. The section worth reading is DevTools for agents: the Chrome DevTools MCP server picked up security controls, performance tuning and memory analysis in one go. The browser debugger is becoming a tool that agents call, and Chrome has started putting locks on it at the same time.

Browser window open on a laptop

The browser debugger is becoming an agent tool, and it is being locked down at the same time.

What this release hands over, and what it takes back

What it hands over: support for the Agent Plugins 1.0 package so agents can load and execute modular plugin extensions; on-demand source maps to stop large sites from blowing up memory; heap snapshot queries by retained size or property name, with self and retained sizes for edges and visibility into memory retained by V8 execution contexts; dedicated PWA automation tools covering install and lifecycle checks; and optional stack traces in list_console_messages.

What it takes back is more interesting. There are new JavaScript execution controls: you can configure --no-javascript-evaluation, or turn the JavaScript execution tools off entirely, so the agent inspects pages without evaluating scripts. The restriction holds across navigations and initialisation scripts. That is the part of this release I care about most.

The reasoning is simple. An agent that can evaluate arbitrary JavaScript inside a browser session you are logged into is an agent that can read your session cookie and post it somewhere. Unless the task is literally JavaScript debugging, that switch should be off by default. The more capable the tool, the more conservative the default should be.

// Wire the Chrome DevTools MCP server into your coding agent. The server exposes
// the browser as tools: navigate, snapshot, console, network, performance, memory.
{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "chrome-devtools-mcp@latest"],

      // Day one changes. The default grants unrestricted filesystem paths and
      // allows JavaScript evaluation. Neither is what you want on a public site.
      "env": {
        "CHROME_DEVTOOLS_MCP_ARGS": [
          "--no-javascript-evaluation",
          "--headless",
          "--isolated"
        ].join(" ")
      }
    }
  }
}
// Confirm the exact flag names against the chrome-devtools-mcp repository before
// you ship this. The CLI gained flags in this release; older docs do not list them.

Agent Plugins 1.0: new capability, new attack surface

Agent Plugins 1.0 lets agents load and execute modular plugin extensions. That is a real capability increase, and it means you are now reviewing plugins, not just prompts. A plugin is code, and code carries permissions.

The manifest is usually short enough that you want to skip it. Do not. The only section that needs real attention is permissions, because that is what determines the worst thing the plugin can do. My rule when writing one is that a permission I do not need is a permission I do not grant: a plugin whose job is recording a performance trace does not need browser.evaluate.

There is a second thing that gets overlooked. Plugins are something an agent can load and execute. Pull one from a community repository and you have added that code to an execution environment with browser access. It is the same risk model as an npm dependency, except people are not in the habit of being suspicious in this particular spot yet.

# Agent Plugins 1.0: agents can load and execute modular plugin extensions. That is
# capability and that is also the new attack surface. An Agent Plugins 1.0 manifest
# is small, so read every field of every plugin you install.
# plugin.yaml — the shape, not a vendor schema
name: chrome-perf-audit
version: 0.1.0
description: Record a performance trace and summarise long tasks for an agent.
entry: ./dist/index.js
permissions:
  - browser.navigate
  - browser.performance.trace
  - filesystem.read:workspace
# Deliberately absent: browser.evaluate. If a plugin does not need JavaScript
# evaluation, do not grant it. Capability you did not grant is capability that
# cannot be abused.
inputs:
  - name: url
    type: string
    required: true
outputs:
  - name: trace_summary
    type: json

Heap snapshot queries: ask a question that has an answer

Asking an agent to find a memory leak used to produce a 900 MB heap snapshot and then a paragraph of nothing useful, because nobody knows which line to look at. Now there is query_heapsnapshot: query objects by retained size or property name, view self and retained sizes for edges, and inspect memory retained by V8 execution contexts.

The tooling changed, so the question has to change. "Find the memory leak" is not a query. "What retains more than 5 MB" is. My usual order is retained size first to find the biggest retainer, then property name once I know what the leaked object holds.

That sounds small and it is not. Agents were effectively useless on memory work because they are good at reading text and bad at reading a few hundred megabytes of dump. Now they can at least ask something the snapshot can answer.

# Heap snapshot inspection is now queryable. Instead of dumping a 900 MB snapshot
# at the agent and hoping, ask a specific question.
# Tool surface: query_heapsnapshot — query objects by retained size or property name,
# view self and retained sizes for edges, inspect memory retained by V8 contexts.

def ask_agent_to_find_the_leak(agent):
    # 1. Take the snapshot after the workflow that you suspect leaks.
    agent.call("performance_take_memory_snapshot", {"reportProgress": True})

    # 2. Ask by retained size first: the biggest retainer is usually the culprit.
    top = agent.call("query_heapsnapshot", {
        "query": "retained_size > 5000000",
        "limit": 20,
    })
    for node in top["nodes"]:
        print(f'{node["retainedSize"] / 1e6:8.1f} MB  {node["name"]}  {node["type"]}')

    # 3. Then by property name, when you know what the leaked object holds.
    leaked = agent.call("query_heapsnapshot", {
        "propertyName": "detachedNode",
        "limit": 50,
    })
    return {"largest": top["nodes"][:5], "byProperty": leaked["count"]}

# The point is the shape of the question. "Find the memory leak" is not a query.
# "What retains more than 5 MB" is.
Terminal output on a dark screen

Find the memory leak is not a query. Retained over 5 MB is.

Filesystem roots: the default you must change

This line is easy to miss. The CLI now includes --allow-unrestricted-paths by default, and it also supports configurable filesystem roots. The first half is permissive by default; the second half is how you narrow it. Out of the box, it is wide.

In CI that combination is genuinely dangerous. CI machines tend to carry deployment keys, registry credentials and cloud tokens. An agent that can write to any path, combined with a page it happened to be influenced by, is a data exfiltration channel.

So my build script does one thing: it passes --filesystem-root explicitly and trusts no default. The build_args function below is the minimal version, where allow_js and roots decide whether this session is read-only inspection or something that can act. Treat it as a policy file, not a preference.

# Security posture for an agent that drives a real browser. These are the controls,
# not the defaults you should trust.
import subprocess, shlex

BASE = "npx -y chrome-devtools-mcp@latest"

def build_args(allow_js: bool, roots: list[str], on_demand_maps: bool = True) -> str:
    args = [BASE]

    # Inspect without evaluating. Applies across navigations and init scripts,
    # so a page cannot re-enable evaluation by navigating.
    if not allow_js:
        args.append("--no-javascript-evaluation")

    # Filesystem roots are configurable. --allow-unrestricted-paths is on by
    # default, which is the opposite of what you want in CI.
    for r in roots:
        args.append(f"--filesystem-root={r}")

    if on_demand_maps:
        args.append("--source-maps=on-demand")

    return " ".join(shlex.quote(a) for a in args)

print(build_args(allow_js=False, roots=["/workspace/reports"]))
print(build_args(allow_js=True, roots=[]))

# Read this as a policy, not a preference. An agent that can evaluate arbitrary
# JavaScript in a logged-in browser session is an agent that can read your session
# cookie and post it somewhere. Turn it off unless the task is literally JS debugging.

Two more things worth automating

First, CPU performance tier overrides. DevTools can now configure, calculate and dispatch calibrated CPU performance tier overrides over CDP via Emulation.setCPUPerformanceOverride, which means CI can finally reproduce "your site is slow on a mid-range phone" instead of arguing about it in meetings. Pin a tier and traces become comparable week to week.

Second, ad script tracking. The Application panel gained a dedicated Ads sub-panel for debugging page-wide ad information: a dashboard for four experimental CrUX metrics (viewport ad density, viewport ad count, total CPU usage by ads, total network usage by ads), CPU and network segmentation by ad elements, a table of ad scripts in the main frame, and a toggle that highlights ads on the page.

If you run ads, those four numbers are the ones to watch, because they are the ones a Core Web Vitals penalty will eventually be computed from. There is a pile of smaller improvements in the same release too: the Elements panel can keep showing style rules that matched before and no longer apply, adorners are keyboard-focusable, drag-and-drop reordering works across shadow DOM boundaries, and CSS property toggling no longer double-repaints. None of it is exciting. All of it gets used daily.

# Two more things worth automating: reproducible perf tiers and ad-cost measurement.
# CPU performance tier overrides are reachable over CDP now, which means CI can
# reproduce "your site is slow on a mid-range phone" instead of arguing about it.
import json, websocket

def override_cpu_tier(ws_url: str, tier: int) -> None:
    ws = websocket.create_connection(ws_url)
    ws.send(json.dumps({
        "id": 1,
        "method": "Emulation.setCPUPerformanceOverride",
        "params": {"tier": tier, "enabled": True},
    }))
    print(ws.recv())
    ws.close()

# tier values map to the calibrated performance tiers DevTools shows in the
# Performance panel. Pin one in CI so a trace is comparable week to week.
override_cpu_tier("ws://127.0.0.1:9222/devtools/page/ABC", tier=3)

# The Application panel now tracks ad scripts with four experimental CrUX metrics:
# viewport ad density, viewport ad count, total CPU usage by ads, total network
# usage by ads. If you run ads, those four numbers are the ones to watch, because
# they are the ones a Core Web Vitals penalty will eventually be computed from.
Dense code on a monitor

An agent that can evaluate arbitrary JavaScript can also read your session cookie.

📌 Frequently Asked Questions

What is the Chrome DevTools MCP server?

It is Chrome's official MCP server, in the ChromeDevTools/chrome-devtools-mcp repository, which lets a coding agent such as Antigravity, Claude, Cursor or Copilot control and inspect a live Chrome browser. It exposes browser capability as tools: navigation, snapshots, console, network, performance and memory.

What does --no-javascript-evaluation actually block?

It lets the agent inspect a page without evaluating scripts, and the restriction is documented as extending across navigations and initialisation scripts, so a page cannot re-enable evaluation by navigating. For inspecting third-party pages or untrusted content, this is the mode you want on.

Why is --allow-unrestricted-paths a dangerous default?

Because out of the box it permits access to unrestricted filesystem paths. CI environments often hold deployment keys and cloud credentials, and an agent that can write anywhere is a data exfiltration path. The CLI supports configurable filesystem roots, so set them explicitly instead of relying on the default.

What should I review in an Agent Plugins 1.0 manifest?

The permissions. A plugin is executable code and the permission list decides its worst case. Grant nothing you do not need: a plugin that records performance traces does not need JavaScript evaluation capability. Treat community plugins with the same suspicion you apply to npm dependencies.

Does query_heapsnapshot replace full heap snapshot analysis?

Not entirely, but it turns the two most common questions into queries: what retains the most memory, and how many objects hold a given property name. An agent that previously produced nothing useful from a few hundred megabytes of dump can now at least return a conclusion you can act on.