| name | chrome-relay |
| description | Chrome Relay: browser automation via Chrome DevTools Protocol, web scraping, form filling, screenshot capture, performance auditing, and headless Chrome workflows |
Chrome Relay — Browser Automation via CDP
When to activate
- Automating browser interactions (login, form filling, navigation sequences)
- Web scraping dynamic content that requires JavaScript rendering
- Taking screenshots of web pages at various viewport sizes
- Auditing web performance (Lighthouse metrics) from within Claude Code
- Monitoring web page changes (price tracking, content updates, availability)
- Testing web applications end-to-end without a full test framework
When NOT to use
- Simple HTTP requests where
curl or fetch suffices (static APIs)
- API testing where dedicated tools (Postman, HTTPie) are better
- When Playwright MCP is already configured and sufficient
- Scraping static HTML that doesn't require JavaScript execution
- Production load testing (use k6, Artillery instead)
Instructions
1. Setup — Chrome DevTools Protocol
google-chrome --remote-debugging-port=9222 --headless=new
npm install puppeteer
Connect from Claude Code:
const puppeteer = require('puppeteer');
async function connectBrowser() {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
const browser = await puppeteer.connect({
browserURL: 'http://localhost:9222'
});
return browser;
}
2. Page Navigation & Interaction
async function scrapePage(url) {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
await page.goto(url, { waitUntil: 'networkidle2' });
const data = await page.evaluate(() => {
return {
title: document.title,
h1: document.querySelector('h1')?.textContent,
links: [...document.querySelectorAll('a')].map(a => ({
text: a.textContent.trim(),
href: a.href
})).filter( => l. && l.),
: {
: .()?.,
: .()?.,
}
};
});
browser.();
data;
}
3. Form Automation
async function fillAndSubmit(page, formData) {
for (const [selector, value] of Object.entries(formData)) {
await page.waitForSelector(selector, { timeout: 5000 });
await page.click(selector, { clickCount: 3 });
await page.type(selector, value, { delay: 50 });
}
await page.select('#country', 'US');
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle2' }),
page.click('button[type="submit"]')
]);
const result = await page.evaluate(() => {
const error = document.querySelector('.error-message');
return error ? { : , : error. } : { : };
});
result;
}
4. Screenshot Capture
async function captureScreenshots(url, viewports) {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const screenshots = {};
for (const [name, viewport] of Object.entries(viewports)) {
await page.setViewport(viewport);
await new Promise(r => setTimeout(r, 500));
screenshots[name] = `screenshots/${name}.png`;
await page.screenshot({
path: screenshots[name],
fullPage: true,
type: 'png'
});
}
await browser.close();
return screenshots;
}
captureScreenshots('https://example.com', {
desktop: { width: 1920, : },
: { : , : },
: { : , : },
});
5. Performance Auditing
async function auditPerformance(url) {
const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.setCacheEnabled(false);
const client = await page.createCDPSession();
await client.send('Performance.enable');
const start = Date.now();
await page.goto(url, { waitUntil: 'load' });
const loadTime = Date.now() - start;
const metrics = await client.send('Performance.getMetrics');
const perfMetrics = metrics.metrics.reduce((acc, m) => {
acc[m.name] = m.value;
return acc;
}, {});
const webVitals = await page.( {
( {
( {
entries = list.();
({
: entries[entries. - ]?.,
: entries[]?. - entries[]?.,
});
}).({ : , : });
( ({ : , : }), );
});
});
browser.();
{
: loadTime,
: perfMetrics[],
: perfMetrics[],
: webVitals,
};
}
6. Network Interception
async function interceptRequests(page) {
const requests = [];
page.on('request', req => {
requests.push({
url: req.url(),
method: req.method(),
resourceType: req.resourceType(),
size: req.headers()['content-length'] || 'unknown',
});
});
await page.setRequestInterception(true);
page.on('request', req => {
if (['image', 'font', 'media'].includes(req.resourceType())) {
req.abort();
} else {
req.continue();
}
});
await page.goto('https://example.com');
return requests;
}
Example
Scraping a product listing page with dynamic content:
User: "Scrape the top 20 products from our competitor's catalog page.
Get name, price, rating, and image URL."
Agent:
1. Launch headless Chrome
2. Navigate to catalog page, wait for product grid to render
3. Scroll to load lazy-loaded products (infinite scroll)
4. Extract product data via page.evaluate:
- Name from .product-title elements
- Price from .product-price (strip currency symbol)
- Rating from .star-rating (count filled stars)
- Image from img.product-image src attribute
5. Block image/font requests for faster scraping
6. Handle pagination (click "Next", wait for new products)
7. Output: JSON array of 20 products with all fields
Anti-Patterns
- No wait strategy: Navigating and immediately scraping without waiting for JS rendering — use
waitUntil: 'networkidle2' or waitForSelector
- Aggressive scraping: Firing 100 requests/second without delays — add random delays (1-3s) between page loads to avoid IP blocking
- Ignoring robots.txt: Scraping pages that explicitly disallow bots — check robots.txt first and respect rate limits
- Missing error handling: Not handling CAPTCHAs, 403s, or cloudflare challenges — always check response status before parsing
- Memory leaks: Keeping browser instances open without closing — always
browser.close() in a finally block
- Hardcoded selectors: Using brittle CSS selectors that break on minor UI updates — prefer semantic selectors (aria-labels, data-testids, role attributes)