Chrome DevTools CLI

SkillWeb & browsing

Lets your agent drive Chrome to open web pages, take screenshots, inspect network traffic, and profile page performance.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Chrome DevTools CLI skill

About this capability

Use when the user asks to "take a screenshot of a website", "navigate to a URL", "fill a form in the browser", "interact with Chrome", or when a chrome automation task is needed.

What this skill tells your AI

The instructions your AI receives, as published by aeroxy/chrome-devtools-cli in skill/chrome-devtools/SKILL.md and read by ahel’s review.

A CLI that talks directly to your running Chrome via the DevTools Protocol.

Prerequisites

The browser must have remote debugging enabled:

  1. Open Chrome (or Edge)
  2. Go to chrome://inspect/#remote-debugging (Edge: edge://inspect/#remote-debugging)
  3. Enable the remote debugging server

This is a persistent, in-browser toggle — not a launch flag. Three things follow from that, and they are what make it the right route for attaching to an everyday browser:

  • It takes effect immediately. The server starts on the already-running process; no restart, no relaunch, nothing to quit.
  • It survives restarts. The choice is stored in the profile's Local State as devtools.remote_debugging.user-enabled, so every later launch serves a port with no flags. To check whether a profile has it on, read that key.
  • It is per-browser and per-profile. Enabling it in Chrome does nothing for Edge, and vice versa — each has its own toggle and its own Local State.

The port is chosen by the browser and recorded in <user-data-dir>/DevToolsActivePort. Read that file rather than assuming a number: it differs per browser and can change between launches. Auto-connect does this for you, which is why no URL is needed. A daemon is spawned on first invocation and reused across commands (5-minute idle timeout).

Microsoft Edge works the same — it is Chromium and speaks the same protocol. Add --browser edge so auto-connect reads Edge's profile instead of Chrome's (--channel still selects stable/beta/dev/canary, except that Edge ships no Canary for Linux — --browser edge --channel canary is rejected there rather than pointed at a directory that cannot exist). --ws-endpoint and --user-data-dir already say where to connect, so --browser is not needed to reach Edge — but pass it anyway when you are targeting Edge, because it is also the label recorded for the daemon (list-daemons) and the browser named in connection errors. Without it both say Chrome, the default, whatever the endpoint points at. Everything below applies unchanged — only the profile location differs.

The --remote-debugging-port launch flag is the other route, and it is for a throwaway instance rather than your everyday browser — see the headless recipe below. Two traps if you reach for it: it is ignored when that profile is already running (the launch hands off to the existing process and no port file appears, so quit first or use a different --user-data-dir), and a fresh Edge profile is not the clean room it looks like — Edge's first-run import pulls open tabs and extensions from your default browser, so a mktemp -d profile can come up holding your real session.

⚠️ Critical: How Page Targeting Works

Targets are NOT arbitrary strings. You cannot use --target main, --target page1, or any made-up name.

Targets are friendly word-pair names (like warm-squid, pink-hen) that the CLI derives from Chrome's internal target IDs. You get them from command output — never invent them.

Names are stable for the lifetime of a tab — navigating within the same tab keeps the same target name; closing and reopening the tab gives a new name.

The Correct Workflow

Step 1: Run list-pages to see what's open and get target names

chrome-devtools list-pages

Output:

[0] (warm-squid) Your Repositories — https://github.com/aeroxy
[1] (pink-hen) Gmail — https://mail.google.com
[2] (hazy-vole) Example — https://example.com

Step 2: Use the friendly name from the output in subsequent commands

chrome-devtools --target warm-squid navigate https://example.com
chrome-devtools --target pink-hen screenshot --output screenshot.png

Alternative: Use --page <index> for numeric indexing (0-based)

chrome-devtools --page 0 navigate https://example.com

If both --target and --page are omitted, the command runs on page 0 (leftmost tab). This is fine for single-tab workflows but should generally be avoided — always pin to a known page.

Core Capabilities

  • Navigation: navigate, navigate --back, navigate --forward, navigate --reload
  • Page management: list-pages, new-page, close-page, select-page
  • Extraction: screenshot, snapshot (accessibility tree), evaluate (JavaScript), read-page (page content as markdown), run-script (run local JS file), adapter (run site adapter)
  • Interaction: click, fill, type-text, press-key, hover, click-at
  • Emulation: emulate (viewport, mobile, geolocation, URL blocking)
  • Inspection: console (logs), network (requests), sw-logs (extension service workers)
  • Third-party tools: list-3p-tools, execute-3p-tool (tools exposed by window.__dtmcp)
  • Synchronization: wait-for (wait for text on page)
  • Daemon control: kill-daemon

Standard Patterns

Pattern 1: Navigate and Interact

navigate and new-page print the target name at the end — capture it to pin subsequent commands.

# 1. List pages to find target
chrome-devtools list-pages

# 2. Navigate (target name shown at end of output)
chrome-devtools --target warm-squid navigate https://example.com
# stdout: Navigated to https://example.com
# stderr: [navigated to: https://example.com]
# stderr: [target:warm-squid]

# 3. Pin all subsequent commands to this page
chrome-devtools --target warm-squid screenshot --output page.png
chrome-devtools --target warm-squid evaluate "document.title"

# 4. Open a new tab — capture the NEW target name from output
chrome-devtools new-page https://github.com
# stdout: Opened: https://github.com
# stderr: [target:icy-goat]  ← new tab, new target
chrome-devtools --target icy-goat snapshot

Note: The [navigated to: ...] and [target:...] lines go to stderr, not stdout. The stdout contains only the main command output ("Navigated to …", "Opened: …").

Pattern 2: Emulation (Viewport & Geolocation)

Overrides are per-tab: each page keeps its own viewport/geolocation/URL-blocks, persisting across navigation within that tab and isolated from other tabs (until cleared, the tab closes, or the daemon exits). emulate with no flags shows the active tab's state.

# Set viewport and geolocation
chrome-devtools --target warm-squid emulate --viewport 1920x1080 --geolocation 40.71,-74.00

# Emulate mobile device
chrome-devtools --target warm-squid emulate --viewport 375x812 --mobile --device-scale-factor 3

# Navigate with emulation (emulation applied before URL loads)
chrome-devtools --target warm-squid navigate https://example.com --viewport 375x812 --mobile

# Open new tab with emulation
chrome-devtools new-page https://example.com --viewport 375x812

# Show current overrides
chrome-devtools --target warm-squid emulate
# Output: No emulation overrides active.  (or lists current blocks/viewport/etc.)

# Clear emulation overrides
chrome-devtools --target warm-squid emulate --clear-all        # clears everything
chrome-devtools --target warm-squid emulate --clear-viewport   # clears viewport only
chrome-devtools --target warm-squid emulate --clear-geolocation

Pattern 3: URL Blocking (Network Debugging)

Block URL patterns using simple * wildcards: *.png (all PNG files), cdn.example.com/* (a domain path), *analytics* (any URL containing "analytics"). Patterns persist in the daemon until cleared.

Scope: blocking applies to subresources the page loads (images, scripts, fetch/XHR, stylesheets, CDN, trackers). It does not block the top-level navigation document itself — e.g. --block-url "*example.com*" then navigate https://example.com still loads the page, but any example.com subresources are blocked. This is a Chrome Network.setBlockedURLs limitation, not a CLI bug.

# Add block patterns
chrome-devtools --target warm-squid emulate --block-url "*.png"
chrome-devtools --target warm-squid emulate --block-url "*.ico" --block-url "*.svg"

# Block while navigating (inline)
chrome-devtools --target warm-squid --block-url "*.png" navigate https://example.com

# Show current blocks
chrome-devtools --target warm-squid emulate
# Output: Blocked URLs:
#           *.png
#           *.ico
#           *.svg

# Remove a specific pattern from the blocklist
chrome-devtools --target warm-squid emulate --unblock-url "*.png"
# (Note: --unblock-url REMOVES that pattern from the blocklist. There is no separate "allowlist".)

# Clear all blocks
chrome-devtools --target warm-squid emulate --clear-blocks
chrome-devtools --target warm-squid emulate --clear-all

Pattern 4: Form Interaction

Two ways to fill inputs — choose based on what the site expects:

  • fill sets the value directly via element.value = .... Fast, no key events. Works for text inputs, textareas, <select>, checkboxes, radio buttons. Often breaks React/Vue apps because these frameworks rely on real input events.
  • type-text dispatches individual keyboard events. Slower but triggers all the input, compositionstart/end, etc. events that frameworks listen for. Use this when fill seems to "not work" on interactive frameworks.
# Click an element by CSS selector
chrome-devtools --target warm-squid click "button.submit"

# Click at viewport-relative coordinates (0,0 is top-left of the visible viewport)
chrome-devtools --target warm-squid click-at 100 200

# Fill a text input (fast, no key events)
chrome-devtools --target warm-squid fill "input.search" "search query"

# Type text one char at a time (use for React/Vue/form-validation sites)
chrome-devtools --target warm-squid type-text "search query" --submit-key Enter

# Press a key or key combo. Examples: Enter, Tab, Escape, ArrowDown,
# Control+A, Meta+C, Shift+Tab, Backspace, Space.
chrome-devtools --target warm-squid press-key Enter
chrome-devtools --target warm-squid press-key Control+A

# Hover over an element
chrome-devtools --target warm-squid hover ".menu-item"

# Wait for text to appear (default timeout: 30 seconds = 30000 ms)
chrome-devtools --target warm-squid wait-for "Results" --timeout 10000

Pattern 5: Console & Network Inspection

The daemon maintains a persistent session for the current page that continuously collects network and console events across commands.

console and network return accumulated events and clear the buffer (drain). A second call immediately after returns empty unless new events arrived. Use this for inspecting what happened; use --duration for live monitoring.

# Navigate (generates network + console events)
chrome-devtools --target warm-squid navigate https://example.com

# Drain accumulated network requests (instant, non-blocking)
chrome-devtools --target warm-squid network

# Drain console messages
chrome-devtools --target warm-squid console

# Filter by console type: log, warning, error, info, debug, exception
chrome-devtools --target warm-squid console --type error --type warning

# Filter network by resource type (case-sensitive): Document, Script, Stylesheet,
# Image, Font, XHR, Fetch, Manifest, Media, WebSocket, Other
chrome-devtools --target warm-squid network --type Fetch --type XHR

# Live monitoring: blocks for N milliseconds, collecting events during that window
chrome-devtools --target warm-squid console --duration 5000
chrome-devtools --target warm-squid network --duration 3000

# Drain instantly (default): returns whatever has accumulated so far
chrome-devtools --target warm-squid console --duration 0

Valid --type values for network: Document, Script, Stylesheet, Image, Media, Font, WebSocket, Manifest, XHR, Fetch, Other.

Pattern 6: JavaScript Evaluation

evaluate runs a JavaScript expression and returns the result. It automatically awaits promises and serializes the return value (objects become JSON, primitives come back as plain text).

# Get the page title
chrome-devtools --target warm-squid evaluate "document.title"

# Await a promise automatically
chrome-devtools --target warm-squid evaluate "fetch('/api/user').then(r => r.json())"

# Get a value as JSON (forces JSON serialization)
chrome-devtools --target warm-squid --json evaluate "performance.navigation"

# Handle a JS dialog (alert, confirm, prompt). Without --dialog-action, eval hangs.
chrome-devtools --target warm-squid evaluate "alert('hi')" --dialog-action accept
chrome-devtools --target warm-squid evaluate "confirm('sure?')" --dialog-action dismiss
chrome-devtools --target warm-squid evaluate "prompt('name')" --dialog-action "my-answer"
# Valid --dialog-action values: "accept", "dismiss", or any prompt-text string.

# Save JS output to a file
chrome-devtools --target warm-squid evaluate "JSON.stringify(performance.timing)" -o /tmp/perf.json

Avoid evaluate for DOM traversal. Use snapshot to read page structure and click/fill to interact.

Pattern 7: Output Formats

All commands default to human-readable text output. Use --json or --toon (compact, LLM-friendly) for structured output.

chrome-devtools list-pages                    # human-readable table (default)
chrome-devtools list-pages --json             # JSON
chrome-devtools list-pages --toon             # TOON (compact)

chrome-devtools --target warm-squid snapshot --toon
chrome-devtools --target warm-squid network --toon --type Fetch

--json and --toon are mutually exclusive.

Pattern 8: Snapshot (Accessibility Tree)

Use snapshot instead of evaluate document.querySelector(...) for understanding page structure.

chrome-devtools --target warm-squid snapshot
chrome-devtools --target warm-squid snapshot --output /tmp/ax-tree.txt
chrome-devtools --target warm-squid snapshot --toon  # compact output

Pattern 9: Screenshots

# Default: viewport-only PNG
chrome-devtools --target warm-squid screenshot --output page.png

# Full scrollable page (can be very tall)
chrome-devtools --target warm-squid screenshot --full-page --output full-page.png

# Save to a specific path
chrome-devtools --target warm-squid screenshot --output /tmp/whatever.jpg

Pattern 10: Extension Service Worker Logs

Browser-level command — no --target needed.

# Collect logs from ALL extension service workers (2s window)
chrome-devtools sw-logs --duration 2000

# Filter by extension ID (from sw-logs output)
chrome-devtools sw-logs --duration 2000 --extension-id abcdef123456

Pattern 11: Third-party Developer Tools

For pages that expose tools via window.__dtmcp.

chrome-devtools --target warm-squid list-3p-tools
chrome-devtools --target warm-squid execute-3p-tool "<tool-name>" '<json-params>'

Pattern 12: Reading Page Content as Markdown

Extract the main article content of a page as clean markdown. Uses Readability to identify the article body and converts it to LLM-friendly markdown with metadata (title, byline, excerpt, URL). Non-article pages (SPAs, dashboards) fall back to converting the full page.

# Read the current page as markdown (title prepended as H1)
chrome-devtools --target warm-squid read-page

# Save to a file
chrome-devtools --target warm-squid read-page --output /tmp/article.md

# JSON output includes metadata fields (title, byline, excerpt, site_name, url)
chrome-devtools --target warm-squid read-page --json

When to use read-page vs snapshot:

  • read-page — you want the page's textual content as readable markdown (articles, docs, wiki pages). Best for summarization, extraction, or feeding content to an LLM.
  • snapshot — you need the full accessibility tree with element IDs, roles, and interactive elements. Best for understanding page structure and finding elements to click/fill.

Pattern 13: Local JS Scripting (run-script)

Evaluate a local JavaScript file inside the page context. Dynamic arguments can be passed as raw positional values at the end of the command or via -a/--arg keys, and are automatically typed and injected into the execution context as ctx.args. Supports comment-based @url auto-navigation.

See the dedicated Custom Scripting Guide for full documentation on script creation, argument parsing, and auto-navigation.

# Run a script with trailing positional arguments (auto-navigates if @url is present)
chrome-devtools --target warm-squid run-script skill/chrome-devtools/examples/search_hn.js -- "Rust"

Pattern 14: Custom Domain-Aware Adapters (adapter)

Run site-specific adapter actions. If the browser is not currently on a matching domain (as defined by @domain comments in the JSDoc header), the CLI auto-navigates to that domain first.

See the dedicated Custom Scripting Guide for full documentation on custom adapters, domain protection, and argument parsing.

# Run an adapter function with positional args (auto-navigates if target domain is mismatch)
chrome-devtools --target warm-squid adapter skill/chrome-devtools/examples/hn_adapter.js search -- "Rust"

Pattern 15: Memory Leak Debugging (Heap Snapshots)

Take two heap snapshots around a suspected leak, diff them per class, then drill into individual node IDs. compare-heapsnapshots and inspect-heapsnapshot-node are fully offline (they parse local files — no Chrome connection needed).

# 1. Take a baseline snapshot
chrome-devtools --target warm-squid take-heapsnapshot --output /tmp/base.heapsnapshot

# 2. Perform the leaky action in the page (click, navigate, etc.), then take a second snapshot
chrome-devtools --target warm-squid take-heapsnapshot --output /tmp/current.heapsnapshot

# 3. Diff: one row per class with added/removed counts and sizes, sorted by size delta
chrome-devtools compare-heapsnapshots --base /tmp/base.heapsnapshot --current /tmp/current.heapsnapshot
# idx,className,addedCount,removedCount,countDelta,addedSize,removedSize,sizeDelta
# 0,Detached HTMLDivElement,120,0,120,46080,0,46080
# ...

# 4. Detail for one summary row (use the idx column): per-node-id adds/removes
chrome-devtools compare-heapsnapshots --base /tmp/base.heapsnapshot --current /tmp/current.heapsnapshot --class-index 0

# 5. Inspect a specific node ID from the detail output
chrome-devtools inspect-heapsnapshot-node --file-path /tmp/current.heapsnapshot --node-id 12345

⚠️ Both snapshots must come from the same Chrome session. The diff matches nodes by V8 heap object ID, which is only stable within a single browser session. Comparing snapshots taken across a Chrome restart (or from different profiles/machines) produces a meaningless result where nearly everything is reported as both added and removed — the CLI prints a warning on stderr when it detects this.

Pattern 16: Headless Chrome (No Login, No Human Approval)

When the flow under test doesn't need the user's cookies/credentials, spawn a throwaway headless Chrome instead of attaching to the user's browser. Because the instance is launched with remote debugging already enabled, no consent prompt ever appears — the whole flow runs unattended.

PROFILE=$(mktemp -d)

# 1. Clear any daemon left over from a previous run of THIS profile. Not needed
#    for isolation — daemons are per-endpoint, so the user's browser is
#    unaffected either way — but a stale one here would hold a dead connection.
#    Best-effort: a scoped kill has to resolve this profile's endpoint, and on a
#    fresh $PROFILE there is none yet, so it exits non-zero and removes nothing.
#    `|| true` keeps that expected failure from aborting the script under `set -e`.
#    Do NOT reach for --all here: it would also stop the user's own daemons.
chrome-devtools --user-data-dir "$PROFILE" kill-daemon --force 2>/dev/null || true

# 2. Spawn headless Chrome with an isolated profile; port 0 = pick a free port
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless=new --remote-debugging-port=0 \
  --user-data-dir="$PROFILE" \
  --no-first-run --no-default-browser-check \
  about:blank &
CHROME_PID=$!

# 3. Cleanup — a trap runs it on every exit path, not just success. The daemon
#    can no longer hijack commands aimed at another browser, but it does hold a
#    CDP connection and an idle timer, so stop it rather than leaking it.
cleanup() {
  # Scoped to this profile: a bare kill-daemon would resolve the user's default
  # Chrome profile and stop their daemon instead of this one. Best-effort for
  # the same reason as step 1 — if Chrome died before writing its port file
  # there is no endpoint to resolve, and a trap must not fail on that.
  chrome-devtools --user-data-dir "$PROFILE" kill-daemon --force 2>/dev/null || true
  kill "$CHROME_PID" 2>/dev/null
  # Chrome shuts down asynchronously, so deleting the profile right after
  # SIGTERM races its teardown and can leave it running against a directory
  # that no longer exists. Wait for the process to actually go — but bounded
  # (5s), then SIGKILL, so a Chrome that ignores SIGTERM can't hang the trap.
  for _ in $(seq 1 20); do
    kill -0 "$CHROME_PID" 2>/dev/null || break
    sleep 0.25
  done
  if kill -0 "$CHROME_PID" 2>/dev/null; then
    kill -9 "$CHROME_PID" 2>/dev/null
  fi
  wait "$CHROME_PID" 2>/dev/null
  rm -rf "$PROFILE"
}
trap cleanup EXIT

# 4. DevToolsActivePort is written only after the DevTools server is
#    actually listening, so the file's existence — not the process being
#    alive — is the readiness signal; connecting earlier races startup.
#    Bounded (30s) and watching the PID so a Chrome that crashes or never
#    starts fails the script instead of hanging it forever.
for _ in $(seq 1 60); do
  [ -f "$PROFILE/DevToolsActivePort" ] && break
  kill -0 "$CHROME_PID" 2>/dev/null || { echo "Chrome exited during startup" >&2; exit 1; }
  sleep 0.5
done
[ -f "$PROFILE/DevToolsActivePort" ] || { echo "Chrome not ready after 30s" >&2; exit 1; }

# 5. Every command needs --user-data-dir pointing at the headless profile;
#    the CLI auto-connects by reading its DevToolsActivePort
chrome-devtools --user-data-dir "$PROFILE" navigate https://example.com
chrome-devtools --user-data-dir "$PROFILE" evaluate 'document.title'
chrome-devtools --user-data-dir "$PROFILE" screenshot --output /tmp/shot.png

Linux path: google-chrome or chromium on $PATH replaces the macOS .app binary path. For a headless Edge, swap the binary for /Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge (Linux: microsoft-edge) — the flags are identical, and --user-data-dir already points the CLI at the right profile, so --browser edge is optional for connecting here. Pass it regardless: without it list-daemons labels the daemon chrome and any connection error names Chrome, which is misleading when you are debugging an Edge run.

⚠️ A temp profile is not automatically a clean room in Edge. Edge's first-run import pulls open tabs and extensions from the default browser, and --no-first-run does not reliably suppress it, so a mktemp -d profile can come up holding a copy of the real browsing session — and the debug port then fronts a signed-in profile. Before assuming isolation, confirm it: run list-pages and check that the tabs are only the ones you opened. Until you have confirmed that, treat the port as fronting the user's real, signed-in session.

One daemon per browser endpoint. A daemon is identified by the endpoint it is attached to, so the user's Chrome, the user's Edge, and each headless instance each get their own — you can drive them concurrently, and a command can never be answered by a daemon attached to a different browser. Two consequences for the recipe above:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
249
Forks
10
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
chrome-devtools
Source
github.com/aeroxy/chrome-devtools-cli