Build browser automation and web scraping with Playwright on your local machine. Includes stealth mode for anti-bot bypass, authenticated sessions, infinite scroll handling, screenshot/PDF generation, and residential IP advantages. Use when: automating browsers, scraping protected sites, testing with real IPs, bypassing bot detection, generating screenshots/PDFs, or troubleshooting "target closed", "element not found", or "navigation timeout" errors.
/plugin marketplace add jezweb/claude-skills/plugin install jezweb-tooling-skills@jezweb/claude-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
README.mdassets/example-template.txtreferences/common-blocks.mdreferences/selector-strategies.mdreferences/stealth-techniques.mdscripts/install-browsers.shtemplates/authenticated-session.tstemplates/basic-scrape.tstemplates/infinite-scroll.tstemplates/pdf-generation.tstemplates/screenshot-capture.tstemplates/stealth-mode.tsStatus: Production Ready ✅ Last Updated: 2026-01-10 Dependencies: Node.js 20+ (Node.js 18 deprecated) or Python 3.9+ Latest Versions: playwright@1.57.0, playwright-stealth@0.0.1, puppeteer-extra-plugin-stealth@2.11.2 Browser Versions: Chromium 143.0.7499.4 | Firefox 144.0.2 | WebKit 26.0
⚠️ v1.57 Breaking Change: Playwright now uses Chrome for Testing builds instead of Chromium. This provides more authentic browser behavior but changes the browser icon and title bar.
Node.js:
npm install -D playwright
npx playwright install chromium
Python:
pip install playwright
playwright install chromium
Why this matters:
playwright install downloads browser binaries (~400MB for Chromium)chromium, firefox, or webkit~/.cache/ms-playwright/import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const title = await page.title();
const content = await page.textContent('body');
await browser.close();
console.log({ title, content });
CRITICAL:
await browser.close() to avoid zombie processeswaitUntil: 'networkidle' for dynamic content (SPAs)timeout: 60000 if needed# Node.js
npx tsx scrape.ts
# Python
python scrape.py
| Feature | Playwright Local | Cloudflare Browser Rendering |
|---|---|---|
| IP Address | Residential (your ISP) | Datacenter (easily detected) |
| Stealth Plugins | Full support | Not available |
| Rate Limits | None | 2,000 requests/day free tier |
| Cost | Free (your CPU) | $5/10k requests after free tier |
| Browser Control | All Playwright features | Limited API |
| Concurrency | Your hardware limit | Account-based limits |
| Session Persistence | Full cookie/storage control | Limited session management |
| Use Case | Bot-protected sites, auth flows | Simple scraping, serverless |
When to use Cloudflare: Serverless environments, simple scraping, cost-efficient at scale When to use Local: Anti-bot bypass needed, residential IP required, complex automation
⚠️ 2025 Reality Check: Stealth plugins work well against basic anti-bot measures, but advanced detection systems (Cloudflare Bot Management, PerimeterX, DataDome) have evolved significantly. The detection landscape now includes:
- Behavioral analysis (mouse patterns, scroll timing, keystroke dynamics)
- TLS fingerprinting (JA3/JA4 signatures)
- Canvas and WebGL fingerprinting
- HTTP/2 fingerprinting
Recommendations:
- Stealth plugins are a good starting point, not a complete solution
- Combine with realistic user behavior simulation (use
stepsoption)- Consider residential proxies for heavily protected sites
- "What works today may not work tomorrow" - test regularly
- For advanced scenarios, research alternatives like
nodriverorundetected-chromedriver
npm install playwright-extra playwright-stealth
For puppeteer-extra compatibility:
npm install puppeteer-extra puppeteer-extra-plugin-stealth
playwright-extra:
import { chromium } from 'playwright-extra';
import stealth from 'puppeteer-extra-plugin-stealth';
chromium.use(stealth());
const browser = await chromium.launch({
headless: true,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
],
});
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
});
Key Points:
--disable-blink-features=AutomationControlled removes navigator.webdriver flagawait page.addInitScript(() => {
// Remove webdriver property
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
// Mock plugins
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5],
});
// Mock languages
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en'],
});
// Consistent permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
});
// Simulate human cursor movement
async function humanClick(page, selector) {
const element = await page.locator(selector);
const box = await element.boundingBox();
if (box) {
// Move to random point within element
const x = box.x + box.width * Math.random();
const y = box.y + box.height * Math.random();
await page.mouse.move(x, y, { steps: 10 });
await page.mouse.click(x, y, { delay: 100 });
}
}
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
];
const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
const context = await browser.newContext({
userAgent: randomUA,
});
import { chromium } from 'playwright';
import fs from 'fs/promises';
// Save session
const context = await browser.newContext();
const page = await context.newPage();
// ... perform login ...
const cookies = await context.cookies();
await fs.writeFile('session.json', JSON.stringify(cookies, null, 2));
await context.close();
// Restore session
const savedCookies = JSON.parse(await fs.readFile('session.json', 'utf-8'));
const newContext = await browser.newContext();
await newContext.addCookies(savedCookies);
Test your setup at: https://bot.sannysoft.com/
const page = await context.newPage();
await page.goto('https://bot.sannysoft.com/', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'stealth-test.png', fullPage: true });
What to check:
navigator.webdriver should be undefined (not false)✅ Use waitUntil: 'networkidle' for SPAs (React, Vue, Angular)
✅ Close browsers with await browser.close() to prevent memory leaks
✅ Wrap automation in try/catch/finally blocks
✅ Set explicit timeouts for unreliable sites
✅ Save screenshots on errors for debugging
✅ Use page.waitForSelector() before interacting with elements
✅ Rotate user agents for high-volume scraping
✅ Test with headless: false first, then switch to headless: true
❌ Use page.click() without waiting for element (waitForSelector first)
❌ Rely on fixed setTimeout() for waits (use waitForSelector, waitForLoadState)
❌ Scrape without rate limiting (add delays between requests)
❌ Use same user agent for all requests (rotate agents)
❌ Ignore navigation errors (catch and retry with backoff)
❌ Run headless without testing headed mode first (visual debugging catches issues)
❌ Store credentials in code (use environment variables)
Playwright v1.56 introduced new methods for capturing debug information without setting up event listeners:
import { test, expect } from '@playwright/test';
test('capture console output', async ({ page }) => {
await page.goto('https://example.com');
// Get all recent console messages
const messages = page.consoleMessages();
// Filter by type
const errors = messages.filter(m => m.type() === 'error');
const logs = messages.filter(m => m.type() === 'log');
console.log('Console errors:', errors.map(m => m.text()));
});
test('check for JavaScript errors', async ({ page }) => {
await page.goto('https://example.com');
// Get all page errors (uncaught exceptions)
const errors = page.pageErrors();
// Fail test if any errors occurred
expect(errors).toHaveLength(0);
});
test('inspect API calls', async ({ page }) => {
await page.goto('https://example.com');
// Get all recent network requests
const requests = page.requests();
// Filter for API calls
const apiCalls = requests.filter(r => r.url().includes('/api/'));
console.log('API calls made:', apiCalls.length);
// Check for failed requests
const failed = requests.filter(r => r.failure());
expect(failed).toHaveLength(0);
});
When to use: Debugging test failures, verifying no console errors, auditing network activity.
The steps option provides fine-grained control over mouse movement, useful for:
// Move to element in 10 intermediate steps (smoother, more human-like)
await page.locator('button.submit').click({ steps: 10 });
// Fast click (fewer steps)
await page.locator('button.cancel').click({ steps: 2 });
const source = page.locator('#draggable');
const target = page.locator('#dropzone');
// Smooth drag animation (20 steps)
await source.dragTo(target, { steps: 20 });
// Quick drag (5 steps)
await source.dragTo(target, { steps: 5 });
Anti-detection benefit: Many bot detection systems look for instantaneous mouse movements. Using steps: 10 or higher simulates realistic human mouse behavior.
This skill prevents 8 documented issues:
Error: Protocol error (Target.sendMessageToTarget): Target closed.
Source: https://github.com/microsoft/playwright/issues/2938
Why It Happens: Page was closed before action completed, or browser crashed
Prevention:
try {
await page.goto(url, { timeout: 30000 });
} catch (error) {
if (error.message.includes('Target closed')) {
console.log('Browser crashed, restarting...');
await browser.close();
browser = await chromium.launch();
}
}
Error: TimeoutError: waiting for selector "button" failed: timeout 30000ms exceeded
Source: https://playwright.dev/docs/actionability
Why It Happens: Element doesn't exist, selector is wrong, or page hasn't loaded
Prevention:
// Use waitForSelector with explicit timeout
const button = await page.waitForSelector('button.submit', {
state: 'visible',
timeout: 10000,
});
await button.click();
// Or use locator with auto-wait
await page.locator('button.submit').click();
Error: TimeoutError: page.goto: Timeout 30000ms exceeded.
Source: https://playwright.dev/docs/navigations
Why It Happens: Slow page load, infinite loading spinner, blocked by firewall
Prevention:
try {
await page.goto(url, {
waitUntil: 'domcontentloaded', // Less strict than networkidle
timeout: 60000, // Increase for slow sites
});
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Navigation timeout, checking if page loaded...');
const title = await page.title();
if (title) {
console.log('Page loaded despite timeout');
}
}
}
Error: Error: Execution context was destroyed, most likely because of a navigation.
Source: https://github.com/microsoft/playwright/issues/3934
Why It Happens: SPA navigation re-rendered the element
Prevention:
// Re-query element after navigation
async function safeClick(page, selector) {
await page.waitForSelector(selector);
await page.click(selector);
await page.waitForLoadState('networkidle');
}
Error: Page returns 403 or shows captcha
Source: https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth
Why It Happens: Site detects navigator.webdriver, datacenter IP, or fingerprint mismatch
Prevention: Use stealth mode (Step 2-7 above) + residential IP
Error: Download starts but never finishes Source: https://playwright.dev/docs/downloads Why It Happens: Download event not awaited, file stream not closed Prevention:
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('a.download-link'),
]);
const path = await download.path();
await download.saveAs('./downloads/' + download.suggestedFilename());
Error: Scroll reaches bottom but no new content loads Source: https://playwright.dev/docs/input#scrolling Why It Happens: Scroll event not triggered correctly, or scroll too fast Prevention:
let previousHeight = 0;
while (true) {
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === previousHeight) {
break; // No more content
}
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(2000); // Wait for new content to load
previousHeight = currentHeight;
}
Error: WebSocket connection to 'ws://...' failed
Source: https://playwright.dev/docs/api/class-browser
Why It Happens: Browser launched without --no-sandbox in restrictive environments
Prevention:
const browser = await chromium.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
expect: {
timeout: 5000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
// Anti-detection settings
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'stealth',
use: {
...devices['Desktop Chrome'],
launchOptions: {
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
],
},
},
},
],
});
Why these settings:
trace: 'on-first-retry' - Captures full trace for debugging failed testsscreenshot: 'only-on-failure' - Saves disk spaceviewport: { width: 1920, height: 1080 } - Common desktop resolution--disable-blink-features=AutomationControlled - Removes webdriver flagimport { chromium } from 'playwright';
import fs from 'fs/promises';
async function scrapeWithAuth() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
// Login
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', process.env.EMAIL);
await page.fill('input[name="password"]', process.env.PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard', { timeout: 10000 });
// Save session
const cookies = await context.cookies();
await fs.writeFile('session.json', JSON.stringify(cookies));
// Navigate to protected page
await page.goto('https://example.com/protected-data');
const data = await page.locator('.data-table').textContent();
await browser.close();
return data;
}
When to use: Sites requiring login, scraping user-specific content
async function scrapeInfiniteScroll(page, selector) {
const items = new Set();
let previousCount = 0;
let noChangeCount = 0;
while (noChangeCount < 3) {
const elements = await page.locator(selector).all();
for (const el of elements) {
const text = await el.textContent();
items.add(text);
}
if (items.size === previousCount) {
noChangeCount++;
} else {
noChangeCount = 0;
}
previousCount = items.size;
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1500);
}
return Array.from(items);
}
When to use: Twitter feeds, product listings, news sites with infinite scroll
async function scrapeMultipleTabs(urls) {
const browser = await chromium.launch();
const context = await browser.newContext();
const results = await Promise.all(
urls.map(async (url) => {
const page = await context.newPage();
await page.goto(url);
const title = await page.title();
await page.close();
return { url, title };
})
);
await browser.close();
return results;
}
When to use: Scraping multiple pages concurrently, comparison shopping
async function captureFullPage(url, outputPath) {
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1920, height: 1080 },
});
await page.goto(url, { waitUntil: 'networkidle' });
await page.screenshot({
path: outputPath,
fullPage: true,
type: 'png',
});
await browser.close();
}
When to use: Visual regression testing, page archiving, documentation
async function generatePDF(url, outputPath) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm',
},
});
await browser.close();
}
When to use: Report generation, invoice archiving, content preservation
async function fillFormWithValidation(page) {
// Fill fields
await page.fill('input[name="firstName"]', 'John');
await page.fill('input[name="lastName"]', 'Doe');
await page.fill('input[name="email"]', 'john@example.com');
// Handle dropdowns
await page.selectOption('select[name="country"]', 'US');
// Handle checkboxes
await page.check('input[name="terms"]');
// Wait for validation
await page.waitForSelector('input[name="email"]:valid');
// Submit
await page.click('button[type="submit"]');
// Wait for success message
await page.waitForSelector('.success-message', { timeout: 10000 });
}
When to use: Account creation, form testing, data entry automation
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
await retryWithBackoff(async () => {
await page.goto('https://unreliable-site.com');
});
When to use: Flaky networks, rate-limited APIs, unreliable sites
All templates are ready-to-use TypeScript files. Copy from ~/.claude/skills/playwright-local/templates/:
basic-scrape.ts - Simple page scrapingstealth-mode.ts - Full stealth configurationauthenticated-session.ts - Login + scrape patterninfinite-scroll.ts - Scroll until no new contentscreenshot-capture.ts - Full page screenshotspdf-generation.ts - PDF exportExample Usage:
# Copy template
cp ~/.claude/skills/playwright-local/templates/stealth-mode.ts ./scrape.ts
# Edit for your use case
# Run with tsx
npx tsx scrape.ts
Documentation Claude can load when needed:
references/stealth-techniques.md - Complete anti-detection guidereferences/selector-strategies.md - CSS vs XPath vs text selectorsreferences/common-blocks.md - Known blocking patterns and bypassesWhen Claude should load these: When troubleshooting bot detection, selector issues, or site-specific blocks
scripts/install-browsers.sh - Install all Playwright browsersUsage:
chmod +x ~/.claude/skills/playwright-local/scripts/install-browsers.sh
~/.claude/skills/playwright-local/scripts/install-browsers.sh
Microsoft provides an official Playwright MCP Server for AI agent integration:
# Initialize AI agent configurations
npx playwright init-agents
This generates configuration files for:
Key Features:
MCP Server Setup:
# Install globally
npm install -g @anthropic/mcp-playwright
# Or add to Claude Desktop config
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@anthropic/mcp-playwright"]
}
}
}
When to use: Building AI agents that need browser automation, integrating Playwright with Claude or other LLMs.
Official Docker images provide consistent, reproducible environments:
Current Image (v1.57.0):
FROM mcr.microsoft.com/playwright:v1.57.0-noble
# Create non-root user for security
RUN groupadd -r pwuser && useradd -r -g pwuser pwuser
USER pwuser
WORKDIR /app
COPY --chown=pwuser:pwuser . .
RUN npm ci
CMD ["npx", "playwright", "test"]
Available Tags:
:v1.57.0-noble - Ubuntu 24.04 LTS (recommended):v1.57.0-jammy - Ubuntu 22.04 LTSRun with Recommended Flags:
docker run -it --init --ipc=host my-playwright-tests
| Flag | Purpose |
|---|---|
--init | Prevents zombie processes (handles PID=1) |
--ipc=host | Prevents Chromium memory exhaustion |
--cap-add=SYS_ADMIN | Only for local dev (enables sandbox) |
Python Image:
FROM mcr.microsoft.com/playwright/python:v1.57.0-noble
Security Notes:
:latest)Claude Code can orchestrate browser automation:
// scrape.ts
import { chromium } from 'playwright';
async function scrape(url: string) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url);
const data = await page.evaluate(() => {
return {
title: document.title,
headings: Array.from(document.querySelectorAll('h1, h2'))
.map(el => el.textContent),
};
});
await browser.close();
console.log(JSON.stringify(data, null, 2));
}
scrape(process.argv[2]);
Claude Code workflow:
npx tsx scrape.ts https://example.com// screenshot-review.ts
import { chromium } from 'playwright';
async function captureForReview(url: string) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url);
await page.screenshot({ path: '/tmp/review.png', fullPage: true });
await browser.close();
console.log('Screenshot saved to /tmp/review.png');
}
captureForReview(process.argv[2]);
Claude Code can then:
import { chromium } from 'playwright';
async function scrapeConcurrently(urls: string[]) {
const browser = await chromium.launch();
// Use separate contexts for isolation
const results = await Promise.all(
urls.map(async (url) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(url);
const title = await page.title();
await context.close();
return { url, title };
})
);
await browser.close();
return results;
}
Performance gain: 10 URLs in parallel takes ~same time as 1 URL
async function setupStealthContext(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
// WebGL fingerprinting defense
screen: {
width: 1920,
height: 1080,
},
// Geolocation (if needed)
geolocation: { longitude: -74.006, latitude: 40.7128 },
permissions: ['geolocation'],
});
return context;
}
async function waitForDynamicContent(page, selector) {
// Wait for initial element
await page.waitForSelector(selector);
// Wait for content to stabilize (no changes for 2s)
let previousContent = '';
let stableCount = 0;
while (stableCount < 4) {
await page.waitForTimeout(500);
const currentContent = await page.locator(selector).textContent();
if (currentContent === previousContent) {
stableCount++;
} else {
stableCount = 0;
}
previousContent = currentContent;
}
return previousContent;
}
| Strategy | Example | When to Use |
|---|---|---|
| CSS | page.click('button.submit') | Standard HTML elements |
| XPath | page.click('xpath=//button[text()="Submit"]') | Complex DOM queries |
| Text | page.click('text=Submit') | When text is unique |
| Data attributes | page.click('[data-testid="submit"]') | Test automation |
| Nth child | page.click('ul > li:nth-child(2)') | Position-based |
| Method | Use Case |
|---|---|
waitUntil: 'load' | All resources loaded (default) |
waitUntil: 'domcontentloaded' | DOM ready, faster for slow sites |
waitUntil: 'networkidle' | No network activity for 500ms (SPAs) |
page.waitForSelector(selector) | Element appears in DOM |
page.waitForLoadState('networkidle') | After navigation |
page.waitForTimeout(ms) | Fixed delay (avoid if possible) |
| Option | Value | Purpose |
|---|---|---|
headless | true/false | Show browser UI |
slowMo | 100 (ms) | Slow down for debugging |
args | ['--no-sandbox'] | Disable sandbox (Docker) |
executablePath | /path/to/chrome | Use custom browser |
downloadsPath | ./downloads | Download location |
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--single-process',
'--disable-gpu',
]
Required:
Optional (Node.js stealth):
Optional (Python stealth):
Docker Images:
mcr.microsoft.com/playwright:v1.57.0-noble - Ubuntu 24.04, Node.js 22 LTSmcr.microsoft.com/playwright/python:v1.57.0-noble - Python variant{
"devDependencies": {
"playwright": "^1.57.0",
"@playwright/test": "^1.57.0",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}
Python:
playwright==1.57.0
playwright-stealth==0.0.1
This skill is based on production web scraping systems:
Solution:
npx playwright install chromium
# Or for all browsers:
npx playwright install
Solution: Add shared memory size
# In Dockerfile
RUN playwright install --with-deps chromium
# Run with:
docker run --shm-size=2gb your-image
Solution: Wait for content to load
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(1000); // Extra buffer
await page.screenshot({ path: 'output.png' });
Solution: Reduce concurrency or add memory
const browser = await chromium.launch({
args: ['--disable-dev-shm-usage'], // Use /tmp instead of /dev/shm
});
Solution:
Use this checklist to verify your setup:
npm list playwright or pip show playwright)npx playwright install chromium)headless: false firstheadless: trueQuestions? Issues?
references/common-blocks.md for site-specific blocksnpx playwright install chromiumThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.