Browser Automation skill for Codex

Automate web chores without storing passwords. Codex scripts repetitive website chores with Playwright: the user signs in once and the session is reused, locators target roles and labels instead of brittle CSS, fixed sleeps become waits on real page state, and irreversible clicks sit behind a dry run and a confirm flag. Every failed run leaves a trace file you can replay step by step.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- browser-automation

Installs to ~/.agents/skills/browser-automation/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $browser-automation, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes Playwright scripts (Python or Node) that automate real web tasks - logging in with a saved session, filling forms, clicking through multi-step flows, downloading reports and exporting data from dashboards that have no API - using role-based locators, auto-waiting, storage state and traces for debugging. Use when asked to automate a website chore, download files from a portal on a schedule, fill a web form from a spreadsheet, or fix a flaky Playwright or Selenium script.

Use it when

  • A website has no API and you repeat the same clicks every week
  • Reports or files must be downloaded from a portal on a schedule
  • A batch of records must be entered through a web form
  • An existing browser script is flaky because of sleeps and brittle selectors

Use something else when

How to check the result

Watch one headed dry run end to end, then run headless three times in a row and confirm each produces the expected file and exits zero.

The mistake it prevents

Hardcoding the user's password and fixed sleeps into the script, which leaks a credential and fails whenever the site is a little slower than usual.

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

--- name: browser-automation description: Writes Playwright scripts (Python or Node) that automate real web tasks - logging in with a saved session, filling forms, clicking through multi-step… license: MIT ---

A browser script is done when it runs headless unattended, waits on page state rather than on timers, never handles the user’s password in code, stops before any irreversible click unless the user has approved it, and leaves a trace you can open when it fails.

Decide first

SituationChoice
The site has an API or the page calls a JSON endpointUse HTTP instead ($web-scraping); faster and sturdier
Writing automated tests for your own app$playwright-e2e (test runner, fixtures, CI)
Screenshots or social images at scale$screenshot-automation
Chores on third-party sites: export, upload, form fill, downloadThis skill, Playwright library API
Existing Selenium script that worksKeep it; migrate only when flakiness justifies the rewrite

Default to Python sync API for one-off chores and Node for projects already on Node. Read AGENTS.md for the project’s runtime. Install needs network (pip install playwright then playwright install chromium, or npm i -D playwright then npx playwright install chromium); ask for approval if the Codex sandbox blocks it. Running the browser against a remote site also needs network.

Workflow

- [ ] 1. Record the flow once: npx playwright codegen https://portal.example.com
- [ ] 2. Have the user log in by hand in a headed browser; save storage state
- [ ] 3. Rewrite codegen output with role/label locators and explicit assertions
- [ ] 4. Add a dry-run flag that stops before the final submit / send / delete
- [ ] 5. Run headed once with the user watching, then headless with tracing on
- [ ] 6. Schedule it (cron / launchd / CI) and alert on failure

Authentication without touching passwords

Never write a password into the script, a prompt or a log. Let the user sign in once in a headed browser, then reuse the session:

# save_session.py - run once, interactively, by the user
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    ctx = browser.new_context()
    page = ctx.new_page()
    page.goto("https://portal.example.com/login")
    input("Log in in the browser window (including MFA), then press Enter here... ")
    ctx.storage_state(path=".auth/portal.json")
    browser.close()

Add .auth/ to .gitignore: the file holds live session cookies. When the session expires, the script should detect the login page and exit with a clear message asking the user to rerun save_session.py, not attempt to log in itself.

The task script

import argparse, sys
from datetime import date
from pathlib import Path
from playwright.sync_api import sync_playwright, expect, TimeoutError as PWTimeout

ap = argparse.ArgumentParser()
ap.add_argument("--headed", action="store_true")
ap.add_argument("--dry-run", action="store_true")
args = ap.parse_args()
out_dir = Path("downloads"); out_dir.mkdir(exist_ok=True)

with sync_playwright() as p:
    browser = p.chromium.launch(headless=not args.headed)
    ctx = browser.new_context(storage_state=".auth/portal.json", accept_downloads=True)
    ctx.tracing.start(screenshots=True, snapshots=True, sources=True)
    page = ctx.new_page()
    page.set_default_timeout(15_000)
    ok = False
    try:
        page.goto("https://portal.example.com/reports")
        if "login" in page.url:
            sys.exit("Session expired: run save_session.py again")
        page.get_by_label("Period").select_option("last-month")
        page.get_by_role("button", name="Generate").click()
        expect(page.get_by_role("status")).to_have_text("Ready", timeout=120_000)
        if args.dry_run:
            print("dry run: stopping before download"); ok = True
        else:
            with page.expect_download() as dl:
                page.get_by_role("link", name="Download CSV").click()
            target = out_dir / f"report-{date.today():%Y-%m}.csv"
            dl.value.save_as(target)
            print(f"saved {target}"); ok = True
    except PWTimeout as e:
        print(f"timed out: {e}", file=sys.stderr)
    finally:
        ctx.tracing.stop(path=None if ok else "trace-failed.zip")
        browser.close()
    sys.exit(0 if ok else 1)

Open a failure with playwright show-trace trace-failed.zip: it shows DOM snapshots, network and console for every step.

Locator rules

UseInstead of
get_by_role("button", name="Save")page.click("#btn-7f3a")
get_by_label("Email"), get_by_placeholder(...)XPath from codegen
get_by_test_id(...) when the site has data-testidnth-child chains
locator("tr").filter(has_text="Invoice 42")Index into a table by position
expect(locator).to_be_visible() / to_have_text()time.sleep(3) / wait_for_timeout

Playwright actions auto-wait for the element to be visible, enabled and stable. Replace every fixed sleep with a wait on the condition you actually need: page.wait_for_url, expect(...), or page.wait_for_response(lambda r: "/api/report" in r.url).

Irreversible actions

Submitting payments, sending messages, deleting records, publishing posts and accepting terms are outward-facing. The script must:

  • default to --dry-run behaviour until the user has seen one headed run end to end;
  • print exactly what will be submitted (recipient, amount, record ids) before the click;
  • require an explicit --confirm flag (or interactive “yes”) for the final action.

Never automate CAPTCHAs, bot checks or MFA prompts. If one appears, stop and report.

Bulk form filling from a file

  • Read rows with csv.DictReader; validate every row before opening the browser.
  • Process one row per page load; after submit, assert the confirmation text and record the row id plus confirmation number in a results CSV.
  • On failure, screenshot (page.screenshot(path=f"fail-{row_id}.png", full_page=True)) and continue; rerunning skips rows already in the results file.
  • Throttle (a second or two between rows) so the site does not rate-limit or lock the account.

Gotchas

  • Codegen locators are a starting point: it often emits brittle CSS for unlabelled controls.
  • New tabs and popups: wrap the click in with page.expect_popup() as pop: and use pop.value.
  • Iframes need page.frame_locator("iframe#payment") before locating inside them.
  • File inputs: get_by_label("Upload").set_input_files("file.pdf"); no native dialog.
  • Headless runs may get a different layout or a bot wall. Set a realistic viewport (viewport={"width": 1440, "height": 900}) and locale; if the site still blocks automation, stop rather than adding stealth plugins.
  • Storage state does not include sessionStorage; apps that keep tokens there need a fresh login each run.
  • macOS launchd and cron have no display: always run headless when scheduled.

Verification

  • Run headed once with the user watching every step of the dry run.
  • Run headless three times in a row; all succeed and produce identical output names.
  • Expire the session (delete the state file copy) and confirm the script exits with the “run save_session.py” message instead of hanging.
  • Confirm downloaded files are non-empty and parse (open the CSV, check the header row).

Report

**Automates:** monthly CSV export from portal.example.com/reports
**Run:** `python export_report.py` (add --headed to watch, --dry-run to stop before download)
**Auth:** saved session in .auth/portal.json (gitignored); rerun save_session.py when it expires
**Output:** downloads/report-YYYY-MM.csv
**On failure:** exit code 1 plus trace-failed.zip, open with `playwright show-trace`

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Browser Automation"
  short_description: "Automate web chores without storing passwords"
  default_prompt: "$browser-automation Write a script that logs into our vendor portal and downloads last month's invoices"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem