plwr
plwr (prounounced PLUR) is a Playwright CLI for browser automation using CSS selectors. Built on playwright-rs.
Install
Homebrew
Crates.io
Dependencies
Requires Playwright:
&&
For video conversion to non-webm formats, install ffmpeg.
AI Agent Skill
plwr includes a skill file that teaches AI coding agents (like Claude Code) how to automate browsers with plwr. The skill covers the full command set, selector syntax, and common patterns.
Copy the skill to your skills directory:
# Claude Code - personal (available across all your projects):
# Claude Code - project-specific (commit to version control):
# OpenCode:
Usage
Start a browser session, navigate, interact, and stop:
Environment variables
| Variable | Effect |
|---|---|
PLAYWRIGHT_HEADED |
Set to any value to run the browser with a visible window |
PLWR_SESSION |
Default session name (default: default) |
PLWR_TIMEOUT |
Default timeout in ms (default: 5000) |
PLWR_IGNORE_CERT_ERRORS |
Set to any value to ignore TLS/SSL certificate errors |
All commands take -S/--session and -T/--timeout as global options,
which override the environment variables.
Starting and stopping
start launches the browser. All other commands require a running session.
Use --headed (or the PLAYWRIGHT_HEADED env var) to show the browser window.
Commands that interact with page content (text, click, wait, eval,
etc.) require a page to be open first via plwr open. Commands that configure
the session (header, viewport) work before any page is opened.
Navigation
open navigates the current page within the existing browser context. Headers,
cookies, and other state are preserved across navigations. There is no separate
goto command — open always reuses the same context. If you need a fresh
context, use plwr stop followed by plwr start and plwr open.
Waiting
Interaction
All interaction commands (click, fill, hover, check, etc.) auto-wait
for the element to appear and become actionable before performing the action,
up to the timeout (-T, default 5000ms). You rarely need an explicit
plwr wait before an interaction — just use the interaction directly:
click and dblclick support modifier keys and mouse button flags:
Supported keys for press: a–z, A–Z, 0–9, Backspace, Tab,
Enter, Escape, Space, Delete, Insert, ArrowUp, ArrowDown,
ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, F1–F12,
Control, Shift, Alt, Meta, and any US keyboard character
(!@#$%^&*()_+-=[]{}\\|;:'",./<>?`~). Chords use +: Control+c,
Shift+Enter, Meta+a.
Clipboard
Copy content from an element to the browser clipboard and paste it at the
currently focused element. Works with text and images (<img>, <canvas>).
Checkboxes and radios
Select dropdowns
Querying
Like interaction commands, text, attr, inner-html, and input-value
auto-wait for the element to appear before reading its value.
Headers
Set extra HTTP headers sent with every request. Headers persist across
navigations within the same session. Can be set before or after open.
Cookies
Viewport
File uploads
Dialogs (alert, confirm, prompt)
Handle native browser dialogs. The next-dialog command registers a one-shot
handler for the next dialog — call it before the action that triggers
the dialog, because dialogs block page execution until handled.
Typical flow:
For prompt() dialogs, the text argument to accept is entered into the
prompt's input field. If omitted, the prompt is accepted with an empty string.
dismiss clicks Cancel, returning null to the page.
Console logs
Capture browser console output (log, warn, error, info, debug). Messages are automatically captured from page load onward, including messages logged before your code runs.
Each entry includes level, ts (timestamp in ms), and args (array of
stringified arguments).
Network requests
Capture all network requests made by the page. Requests are automatically captured from page load onward, including document, CSS, JS, images, fonts, fetch, XHR, and WebSocket connections. Status codes are available for all resource types.
Available types: doc, css, js, img, font, media, fetch, xhr,
ws, wasm, manifest, other.
Each entry includes type, url, status (HTTP status code), method
(for fetch/XHR/doc), size (transfer size in bytes), duration (ms), and
ts (timestamp in ms).
Computed styles
JavaScript
Simple expressions are evaluated directly:
For multi-statement logic, use an IIFE (immediately invoked function expression):
Walk the DOM, gather computed styles, inspect layout:
Objects and arrays are returned as pretty-printed JSON. Primitives (strings, numbers, booleans) are printed as plain text.
DOM tree
Screenshots
Video
Record a session by passing --video to start. The video is saved when
stop is called. Non-webm formats (e.g. .mp4) require ffmpeg.
# ... do stuff ...
Sessions
Run multiple independent browser sessions in parallel:
Selectors
Playwright uses its own selector engine that extends CSS. Most standard CSS
selectors work directly, but some advanced pseudo-classes need a css= prefix
to bypass Playwright's parser.
Basics
Combinators
Attribute selectors
Unquoted attribute values work directly. For quoted values, use the css=
prefix (see css= prefix below).
Pseudo-classes that work without prefix
Playwright extensions
These are Playwright-specific and don't exist in standard CSS:
The >> operator chains selectors — each segment is scoped to the previous
match. You can mix CSS and Playwright engines:
css= prefix
Playwright's selector parser auto-detects whether a string is CSS, XPath, or a
Playwright selector. Some valid CSS pseudo-classes confuse the auto-detection
because Playwright tries to interpret parenthesized arguments or quoted strings
as its own syntax. Prefixing with css= forces native CSS evaluation.
Need css= prefix:
| Selector | Example |
|---|---|
:last-of-type |
css=.list span:last-of-type |
:first-of-type |
css=.list p:first-of-type |
:nth-of-type() |
css=span:nth-of-type(2) |
:nth-last-child() |
css=li:nth-last-child(1) |
:is() |
css=:is(.card, .sidebar) |
:where() |
css=:where(.card, .sidebar) > p |
Quoted [attr="val"] |
css=[data-testid="login-form"] |
Work without prefix (Playwright recognizes these natively):
:nth-child(), :first-child, :last-child, :not(), :has(), :empty,
:checked, :disabled, :enabled, :required, :visible, :has-text(),
text=, >> nth=N.
Strict mode
Playwright locators are strict by default — if a selector matches multiple
elements, commands like text, click, and attr will fail. Use >> nth=N
or :nth-match() to pick one:
Shell quoting
Watch out for shell metacharacters in selectors. The $ in $= will be
interpreted by bash if not single-quoted:
Example: cctr e2e test
Before (with raw playwright-cli run-code):
===
send a message
===
./pw --session=e2e run-code "async page => {
const input = await page.waitForSelector('.chat-input', { timeout: 2000 });
await input.fill('Hello agent');
await page.keyboard.press('Enter');
await page.waitForFunction(() => {
const msgs = document.querySelectorAll('[data-role=assistant]');
return Array.from(msgs).some(m => m.textContent.includes('Hi'));
}, { timeout: 5000 });
}"
---
After (with plwr):
===
send a message
===
plwr fill '.chat-input' 'Hello agent'
plwr press Enter
---
===
agent responds
===
plwr wait '[data-role=assistant]:has-text("Hi")' -T 10000
---