Browser Automation skill for Claude Code

Script browser chores that resume, not break. Claude checks for an API first, then scripts the chore in Playwright with role-based locators, condition waits instead of sleeps, saved session state, retries with backoff and a progress ledger so reruns skip finished items. It keeps credentials out of code, throttles politely, and pauses for a human at MFA or CAPTCHA prompts rather than trying to get around them.

  • namebrowser-automation
  • size183 lines · ≈2,050 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Writes Playwright (or Puppeteer) scripts that automate repetitive browser chores — logging in with saved session state, filling forms, exporting reports, downloading files, uploading batches — with stable role-based locators, auto-waiting instead of sleeps, retries, idempotent progress tracking, traces for debugging, and strict respect for site terms, rate limits, authentication, and CAPTCHAs. Use when asked to automate a web task, script a download or form submission, replace a manual browser workflow, or fix a flaky automation script (not end-to-end testing).

Try asking

  • Write a script that logs into our billing portal and downloads every invoice PDF from this year
  • Automate uploading these 200 product images through the vendor's web form
  • My Puppeteer script breaks every week when the page changes, make it more reliable

Use it when

  • A repetitive web task has no API or export option
  • You need to batch downloads, uploads or form submissions
  • An existing browser script is flaky or brittle
  • A browser task should run unattended on a schedule

Skip it when

How to check the result

A dry run lists the intended actions, one real item completes end to end, and killing and rerunning the batch resumes without duplicates.

The common mistake it prevents

Fixed sleeps and recorded CSS paths, which pass on the first run and then fail whenever the page is slow or the markup shifts.

~/.claude/skills/browser-automation/SKILL.md

--- name: browser-automation description: Writes Playwright (or Puppeteer) scripts that automate repetitive browser chores — logging in with saved session state, filling forms,… license: MIT ---

Done well means: the script finishes the chore unattended, survives slow pages and minor layout changes, can resume after a crash without redoing finished work, never stores passwords in code, behaves like a considerate human user, and stops at any barrier the site put there on purpose.

Hard rules

  • Check for an API, export feature, or bulk download first. A supported API is faster, more reliable, and usually what the terms of service want you to use.
  • Automate only accounts and data the user is authorized to access, and only within the site’s terms. If the terms prohibit automated access, say so and stop.
  • Never bypass authentication, paywalls, CAPTCHAs, bot detection, or rate limits. Do not use CAPTCHA-solving services, stealth plugins, or fingerprint spoofing. When a CAPTCHA or MFA prompt appears, pause for the human to complete it (headed mode) or stop.
  • No credentials in source. Read them from environment variables or a secret manager, and keep saved session files (storageState) out of git with mode 600; they are as sensitive as passwords.
  • Throttle: one action at a time per site, with pauses; no parallel hammering. Stop on 429 or error pages instead of retrying in a tight loop.
  • Actions that submit, purchase, delete, or send need a dry-run mode and explicit user confirmation before the first real run.

Tool choice

NeedDefault
New script, any languagePlaywright (Node/TypeScript or Python); auto-waiting, downloads, traces, multiple browsers
Existing Puppeteer code, Chrome onlyKeep Puppeteer; apply the same patterns
Read-only data from static pagesPlain HTTP + HTML parser (see web-scraping)
Testing your own appplaywright-e2e, not this skill
One-off task in the user’s real browserA browser-control tool with the user watching

Workflow

- [ ] 1. Confirm authorization, terms, and that no API/export exists
- [ ] 2. Walk through the task manually; list steps, inputs, and outputs
- [ ] 3. Record a draft with codegen; rewrite into stable locators
- [ ] 4. Handle login via saved storage state
- [ ] 5. Add waits on conditions, retries, throttling, progress tracking
- [ ] 6. Dry-run on one item in headed mode; then a small batch
- [ ] 7. Run unattended with logs, traces on failure, and a summary

3. Locators

npx playwright codegen https://example.com/reports     # record clicks into code
npx playwright install chromium                         # browsers, once

Rewrite recorded selectors into user-facing locators, in this order of preference:

  1. page.getByRole('button', { name: 'Export CSV' })
  2. page.getByLabel('Start date'), getByPlaceholder, getByText (exact where possible)
  3. page.getByTestId('export') if the site has test IDs
  4. CSS scoped to a stable container; never long XPath chains or nth-child paths

Scope repeated elements: page.getByRole('row', { name: /INV-1042/ }).getByRole('link', { name: 'Download' }).

4. Login once, reuse session

// login.ts — run headed; the human completes MFA or CAPTCHA if prompted
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://app.example.com/login');
await page.getByLabel('Email').fill(process.env.APP_USER!);
await page.getByLabel('Password').fill(process.env.APP_PASS!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('**/dashboard', { timeout: 120_000 });   // time for manual MFA
await context.storageState({ path: '.auth/state.json' });
await browser.close();

Reuse with browser.newContext({ storageState: '.auth/state.json' }). Detect expiry (redirect to /login) and exit with a clear “re-run login” message rather than attempting to automate around MFA. Add .auth/ to .gitignore.

5. Main script pattern

import { chromium, type Page } from 'playwright';
import { existsSync, readFileSync, appendFileSync, mkdirSync } from 'node:fs';

const DRY_RUN = process.argv.includes('--dry-run');
const DONE = 'progress/done.txt';                 // idempotent resume ledger
mkdirSync('progress', { recursive: true }); mkdirSync('downloads', { recursive: true });
const done = new Set(existsSync(DONE) ? readFileSync(DONE, 'utf8').split('\n').filter(Boolean) : []);
const pause = (ms: number) => new Promise(r => setTimeout(r, ms + Math.random() * ms));

async function withRetry<T>(fn: () => Promise<T>, tries = 3): Promise<T> {
  for (let i = 1; ; i++) {
    try { return await fn(); }
    catch (e) { if (i >= tries) throw e; console.warn(`retry ${i}:`, (e as Error).message); await pause(2000 * i); }
  }
}

async function downloadInvoice(page: Page, id: string) {
  const row = page.getByRole('row', { name: new RegExp(id) });
  if (DRY_RUN) { console.log(`DRY: would download ${id}`); return; }
  const [download] = await Promise.all([
    page.waitForEvent('download'),
    row.getByRole('link', { name: 'Download' }).click(),
  ]);
  await download.saveAs(`downloads/${id}.pdf`);
}

const browser = await chromium.launch();
const context = await browser.newContext({ storageState: '.auth/state.json', acceptDownloads: true });
await context.tracing.start({ screenshots: true, snapshots: true });
const page = await context.newPage();
page.setDefaultTimeout(30_000);
let ok = 0, failed: string[] = [];
try {
  await page.goto('https://app.example.com/invoices');
  if (page.url().includes('/login')) throw new Error('session expired: run login.ts');
  const ids = await page.getByRole('row').filter({ hasText: /INV-\d+/ })
    .evaluateAll(rows => rows.map(r => r.textContent!.match(/INV-\d+/)![0]));
  for (const id of ids) {
    if (done.has(id)) continue;
    try {
      await withRetry(() => downloadInvoice(page, id));
      if (!DRY_RUN) appendFileSync(DONE, id + '\n');
      ok++;
    } catch (e) { failed.push(id); console.error(`failed ${id}:`, (e as Error).message); }
    await pause(1500);                               // be polite
  }
} finally {
  await context.tracing.stop({ path: failed.length ? 'trace.zip' : undefined });
  await browser.close();
  console.log(`done: ${ok} ok, ${failed.length} failed ${failed.join(' ')}`);
  process.exitCode = failed.length ? 1 : 0;
}

Key points:

  • Wait on conditions (waitForURL, expect(locator).toBeVisible(), waitForResponse), never fixed waitForTimeout sleeps.
  • Start waiting for a download or popup before the click that triggers it (Promise.all).
  • Record progress after each item so a rerun skips finished work.
  • Uploads: locator.setInputFiles(path); for a file chooser dialog, page.waitForEvent('filechooser').
  • Python equivalent: playwright.sync_api, page.expect_download() context manager, context.storage_state(path=...).

Debugging

npx playwright show-trace trace.zip    # timeline, DOM snapshots, network, console
PWDEBUG=1 node script.js               # Playwright Inspector, step through

Run headed (headless: false, slowMo: 250) while developing. Save a screenshot and the page URL on each failure.

Verification

  • Dry run lists the intended actions for a sample without side effects.
  • One real item end to end; check the output file or the record in the target system.
  • Kill the script mid-batch, rerun, and confirm it resumes without duplicates.
  • Expired session produces a clear message and non-zero exit, not a crash loop.
  • Final summary counts match the source list.

Gotchas

  • Sites change markup; role and label locators break far less often than CSS paths.
  • Infinite scroll and pagination: loop until “no more” with a hard upper bound.
  • Downloads in headless mode need acceptDownloads (default true in recent Playwright contexts) and saveAs; the temp file disappears when the context closes.
  • Time zones and locales affect date pickers and parsed text; set locale and timezoneId in the context explicitly.
  • Running on a schedule: put the script behind a lock and heartbeat (see scheduled-jobs), and alert when the session expires instead of failing silently every night.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc