playwright-rust
CLAUDE.md166 starsChanged 5 days agoApache-2.0View on GitHub
Guidance for Claude Code working in this repository. playwright-rust — Rust language bindings for Microsoft Playwright, following the same architecture as playwright-python / java / dotnet. JSON-RPC to the official Playwright server (we don't reimplement browser protocols). Goal: production-quality bindings, full Python API parity (achieved in v0.12.0), then v1.0 after multi-month dogfooding. See WHY.md for vision, docs/roadmap.md for direction, and docs/implementation-plans/v1.0-gap-analysis.md for current state. docs/architecture/model.c4 is the codebase map: every crate, every module, and every reference between them that a…
- Installs packages
In this repository5 other files it gives its agents.
- doctest-conventionsSkill
- driver-bumpSkill
- release-processSkill
- supply-chainSkill
- playwright-rs-usageSkill
The file
We split this CLAUDE.md into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 182
Always check Playwright's official API docs first (and playwright-python as the reference implementation).
- Distinctive: line 184
Default to TDD: write the failing test, make it pass, refactor.
- Very specific: line 200
No unsafe code without a // SAFETY: justification
- Very specific: line 206
When the driver drops a parameter, deprecate rather than remove: #[deprecated(since = "<next release>", note = "...")] on the field and its …
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
Guidance for Claude Code working in this repository.
Project
playwright-rust — Rust language bindings for Microsoft Playwright, following the same architecture as playwright-python / java / dotnet. JSON-RPC to the official Playwright server (we don't reimplement browser protocols). Goal: production-quality bindings, full Python API parity (achieved in v0.12.0), then v1.0 after multi-month dogfooding.
See WHY.md for vision, docs/roadmap.md for direction, and docs/implementation-plans/v1.0-gap-analysis.md for current state.
Repository Layout
crates/playwright/ single crate (consolidated from playwright-core in v0.7)
src/api/ launch options, connect options
src/protocol/ protocol objects (Page, Browser, Locator, ...)
src/server/ connection, transport, channel, object factory
src/assertions.rs expect API (auto-retry assertions)
src/error.rs error types
tests/integration/ integration tests
examples/ usage examples
fuzz/ cargo-fuzz targets
build.rs assembles the pinned driver (npm + Node, ADR 0006)
crates/playwright-rs-macros/ locator!() proc macro (published separately)
crates/playwright-rs-trace/ trace-zip parser (published separately)
crates/xtask/ repo tasks (verify-driver-version, verify-changelog-links,
site snippets); publish = false
crates/site/ playwright-rust.dev landing page (Leptos/WASM, built by Trunk)
crates/site-e2e/ dogfoods the bindings against that site; also the deploy gate
supply-chain/ cargo-vet audit config (see skill)
docs/ roadmap, ADRs, implementation plans, technical notes
docs/architecture/ LikeC4 model of every crate and module (generated)
docs/agent/ agent-integration guidance for downstream users
skills/ the skill we ship to downstream users
.claude/skills/ contributor-only skills, plus a symlink to the abovedocs/architecture/model.c4 is the codebase
map: every crate, every module, and every reference between them that a
survey of the source can see, with the path each came from. Read it before
changing a part of the tree you do not know, but treat a missing edge as
unknown rather than absent: macro bodies that are not plain Rust (Leptos's
view!), method calls on values, and #[cfg(test)] items are invisible to
it. It is generated by asbuilt from
the code alone, so never edit it: fix a red asbuilt check by re-running
asbuilt survey with the release asbuilt.toml pins (any other refuses
to run and names it). The curated
views in views.c4 beside it are hand-written.
docs/development.md has the
recipe for a red check; the tool's own docs have the rest.
crates/site and crates/site-e2e are excluded from the workspace
(wasm target; features that would conflict with the root build). Two
consequences worth knowing before they cost you time:
- Workspace-wide commands miss them.
cargo fmt/clippy --workspaceandcargo nextest run --workspacenever reach these crates; only the per-manifest steps inpages.ymldo. After touching either crate, run those four commands (--manifest-path crates/site{,-e2e}/Cargo.toml, plus--target wasm32-unknown-unknownforsite) rather than trusting a green workspace run. They carry their own
Cargo.lock.site-e2edepends onplaywright-rsby path, so a workspace dependency change silently staleness its lockfile, which then refreshes the next time anyone runs a cargo command there — surfacing as unrelated-looking dirt ingit status. A pre-commit hook (scripts/check-external-lockfiles.sh) now flags it on the commits that cause it, with the refresh command. Nothing breaks meanwhile; no CI job uses--lockedagainst these crates. Dependabot's cargo entry lists all four lockfile directories, so routine bumps reach them, andscripts/audit-lockfiles.sh(CI and pre-commit) runscargo auditagainst each lockfile rather than the root alone.scripts/deny-lockfiles.shdoes the same for deny's bans, licenses, and sources. A new excluded tree goes into both scripts and the lockfile hook.crates/playwright/fuzzhas the same shape (its own excluded workspace, its own lockfile, a path dep onplaywright-rs) and is covered by the same hook. It drifts worse than the site crates because nothing routine touches it: it sat onplaywright-rs0.13.0 until 0.15.1 before anyone noticed.
Skills (procedural reference)
Two audiences, kept apart by directory. skills/ is what this repo
ships to the world; .claude/skills/ is contributor-only. Claude Code
auto-discovers only the latter, so the shipped skill is symlinked into it
(.claude/skills/playwright-rs-usage -> ../../skills/playwright-rs-usage)
and there is still one source of truth. On a Windows checkout without
symlink support that link lands as a stray text file and the skill simply
does not auto-load in-repo, which costs nothing else.
The directory split alone does not keep the contributor skills
unpublished. The Agent Skills CLI scans .claude/skills/ as well as
skills/ and happily offered all four to downstream users. Each
contributor-only skill therefore carries metadata.internal: true, which
hides it from npx skills add unless the caller sets
INSTALL_INTERNAL_SKILLS=1. Any new skill added under
.claude/skills/ needs that line, or it ships to everyone who installs
from this repo. Note the value must be the boolean true, not the string
"true": the CLI ignores the quoted form, even though the Agent Skills
spec describes metadata as string-to-string. Verify with
npx skills add <path-to-this-repo> --list, which should report exactly
one skill.
Load these when the task touches their domain:
- supply-chain —
cargo audit/cargo deny/cargo vetworkflow. Read before bumping our own version, before resolving a dependabot PR's vet failures, or when aRUSTSEC-*advisory drops. - doctest-conventions —
no_rundoctests with hidden scaffolding; compile-checked everywhere. Read before authoring or modifying rustdoc examples. - release-process — end-to-end release runbook including the push-commit-then-wait-for-CI-then-tag pattern. Read before driving a release manually.
- driver-bump — moving the pinned Playwright driver: establishing
the delta the release notes leave out, porting upstream regression
tests, the files and trace fixture that move. Read before touching
PLAYWRIGHT_VERSION. - playwright-rs-usage — procedural reference for using
playwright-rs as a downstream Rust dependency (object model,
locator!()macro, builder pattern, auto-wait semantics, trace capture). This is the shipped one, canonical atskills/playwright-rs-usage/. Loaded automatically in sessions running in this repo, via the symlink.
Documentation Hierarchy
Just-in-time philosophy — write the right thing in the right file:
- README.md — landing page; vision, working example (current code only), what works now, installation. Keep < 250 lines. No future API previews.
- docs/roadmap.md — what 1.0 means, what gates it, and what is deliberately out of scope. No per-release status and no slice details; it is not edited when a version ships.
- docs/implementation-plans/vX.Y-*.md — detailed work tracking for the version currently in progress; created just-in-time. Becomes a historical reference once the version ships.
- docs/adr/####-*.md — architecture decisions with trade-off analysis. Use docs/templates/TEMPLATE_ADR.md.
- Rustdoc — every public API gets a summary, link to Playwright
docs (
See: <https://playwright.dev/...>), errors section, and any Rust-specific behavior notes. Examples go in module-level doctests per the doctest-conventions skill, not on individual functions. skills/playwright-rs-usage/SKILL.md— the single agent-facing artifact distributed to downstream Rust projects. Installed withnpx skills add padamson/playwright-rust, the one channel it ships through.docs/agent/CLAUDE_SNIPPET.mdis now only a pointer to it: it used to be a hand-synced copy-paste duplicate, which is exactly the shape that goes stale and then teaches the old API. There is no second copy to keep in sync any more, so don't reintroduce one.Two build gates hold the skill to the code, and they check different things.
cargo xtask verify-agent-docscompiles itsrust,no_runblocks against the real crate, catching code that stopped working; it then asserts the skill names every API-gating cargo feature and browser engine, catching a capability that shipped undocumented. Neither checks that the surrounding prose is accurate. That residue is real, and the skill says so itself.Editing the skill means bumping
metadata.versionin its frontmatter, written as a double-quoted string.npx skills addcopies the skill and has no version of its own, so that field is the only version an installed copy carries, and until it moves a content change reaches nobody. A pre-commit hook (scripts/check-skill-version-bumped.sh) enforces it, and theSkill version guardjob intest.ymlruns the same script against the PR base or pre-push tip, so a hookless clone cannot slip past. The Claude Code plugin channel (.claude-plugin/manifests,/plugin install) was retired in favor of this one channel; do not re-add a manifest.
Working on Features
- Always check Playwright's official API docs first (and playwright-python as the reference implementation).
- Default to TDD: write the failing test, make it pass, refactor. For new APIs that's Red → Green → Refactor against the cross-browser integration suite + an API-compatibility check against playwright-python.
- Match Playwright's API exactly across languages — same method
names, same semantics. Diverge only for idiomatic Rust where
compatibility allows (
Result<T>, builders for option-heavy methods, async/await).
API Conventions
Result<T>consistently; oneErrorenum (crate::error::Error)- Builder pattern for option-heavy methods (matches Playwright's
LaunchOptions,GotoOptions,ClickOptionsstyle) - Locators auto-wait for elements; assertions auto-retry — see the
expect API (
crate::assertions) - No unsafe code without a
// SAFETY:justification - No
unwrap()/expect()on fallible paths reachable from public APIs — return anErrorvariant. Two sanctioned exceptions:std::synclock acquisition (poisoning means another thread already panicked; propagating that panic is the policy) and invariants guaranteed by construction (comment why at the call site) - When the driver drops a parameter, deprecate rather than remove:
#[deprecated(since = "<next release>", note = "...")]on the field and its setter, naming the replacement; stop sending it (#[serde(skip_serializing)], which keeps a deserializing struct loading old config); take it out of the spec gate's exception list. The crate's own uses of a deprecated field still warn, so wrap each in#[allow(deprecated)]. Then add the item to the removal list in the v1.0.0 section of the gap analysis, which is the only place removal is scheduled
Testing
- Unit tests — protocol serialization, connection management,
server lifecycle (in
crates/playwright/src/) - Integration tests — end-to-end API exercising real browsers
(
crates/playwright/tests/integration/); usecommon::setup()/common::setup_context()helpers. To wait for an event/state change, usecommon::poll_until(timeout, cond)— never a fixedtokio::time::sleepbefore an assertion (flakes on loaded CI) - Doctests — see the doctest-conventions skill
- CI runs Linux, macOS, Windows with Chromium + Firefox + WebKit.
Firefox/WebKit and stress tests are
#[ignore = "..."]with the lane named in the reason; CI runs them with--run-ignored ignored-only - No coverage job, on purpose. Mutation testing (below) subsumes line coverage for tested code; a lint on never-executed lines is not wanted
Development Commands
# Tests (cargo-nextest required: cargo install cargo-nextest)
cargo nextest run # all tests
cargo nextest run -p playwright-rs --lib # unit tests only (~2s, no browsers)
cargo nextest run -p playwright-rs -E 'test(locator)'
# Doctests (nextest does not run these; no_run = compile-only by design)
cargo test --doc --workspace # pre-commit and CI run exactly this
# Examples
cargo run --package playwright-rs --example basic
# Quality
cargo fmt -- --check
cargo clippy --workspace --all-targets -- -D warnings
# Local CI rehearsal
prek run --all-filesClaude Code sandbox
The sandbox is on (user settings) and .claude/settings.json (tracked)
carries this repo's additions: git, gh, cargo nextest run,
cargo run, scripts/mutants.sh, and prek run outside it because they
need credentials or the browser's Mach ports the sandbox hides; loopback
is allowed for the test servers; the configd Mach lookup lets
cargo vet/audit/deny run sandboxed; and the trustd.agent lookup
lets cargo audit verify TLS through the macOS platform verifier, without
which it cannot fetch its database (OSStatus -26276). Keep that file to
the sandbox block. Personal permission allowlists go in the untracked
.claude/settings.local.json. A session here never writes into a sibling
checkout. Hooks are installed once per clone by hand, since .git/hooks
is outside the sandbox's write set:
cargo install prek
prek install --overwrite # replaces any legacy pre-commit hookManual checks
What a session cannot check and the person reviewing the work can. A hand-off before a commit names each one that applies, as its command and what a pass looks like; CI is the backstop, not the plan.
From a plain terminal (the sandbox blocks these):
- Any change under
crates/site/orcrates/site-e2e/:crates/site/preview.sh. It renders the architecture tree, builds the site, runs thesite-e2etests (which launch Chromium, and Chromium dies in the sandbox), and then serves the page with hot reload. A pass is every test green. It needstrunk,asbuilt, Node, and Graphviz onPATH, and Chromium installed once withcargo run -p playwright-rs --features cli -- install chromium. - A new public API's rustdoc:
cargo doc -p playwright-rs --open, sinceopenis blocked in the sandbox.
Not from a session at all, changed on github.com or crates.io: the
main and release tags rulesets (the latter covers v*, macros-v*,
and trace-v*, admin bypass only), the merge App's Dependabot secrets,
Dependabot alerts and security updates, the Pages source, and each crate's
Trusted Publisher.
By eye:
- A walkthrough change: the PNGs
preview.shwrites tocrates/site/public/receipts/, one distinct outcome per step, legible. - Prose a reader will act on: the CHANGELOG entry, a README section, and
the shipped skill.
cargo xtask verify-agent-docscompiles the skill's code and checks the names it gives; nothing checks that its prose is still true.
Mutation testing
scripts/mutants.sh wraps cargo mutants --in-diff, scoping mutation
testing to just the lines a commit touched. A full-codebase run grows
linearly with codebase size and routinely takes hours; --in-diff
keeps the loop fast enough to use while the test is still warm.
./scripts/mutants.sh # diff HEAD~1..HEAD (default)
./scripts/mutants.sh main # diff main..HEAD
./scripts/mutants.sh -- --jobs 4 # pass extra cargo-mutants argsCI runs the per-diff variant on every push and PR
(mutation-testing-diff in security.yml).
The full-codebase job (mutation-testing) runs on the weekly
Saturday cron and on demand via workflow_dispatch — kept on a cadence
so test-quality drift across files outside the recent diff still gets
caught. Release tags are not a trigger: security.yml filters
on.push to branches, which excludes tag pushes entirely. Dispatch a
run manually if you want full coverage before cutting a release.
Scope is set by .cargo/mutants.toml
(examine_globs lists the files that get mutated at all; exclude_re
removes mutants that are only testable via integration tests;
additional_cargo_test_args names the unit tests and every browser-free
test binary, so a new one must be added there to count). --in-diff
narrows from there. ./scripts/mutants.sh --working gates uncommitted
edits before the commit exists.
Install once: cargo install cargo-mutants.
Versioning
0.x.y while pre-1.0; API may evolve. 1.0.0 when the gates in
docs/roadmap.md all hold. For release mechanics see the
release-process skill.
Useful References
- Playwright docs: https://playwright.dev/docs/api
- playwright-python (reference impl): https://github.com/microsoft/playwright-python
- Playwright server source: https://github.com/microsoft/playwright/tree/main/packages/playwright-core/src/server
- Driver protocol schema — the wire contract this crate implements, and
the authoritative thing to diff when bumping the driver. It is not
in the driver we assemble (the
playwright-corenpm package ships no schema), so it is vendored inprotocol-spec/at the pinned tag: 19.ymlfiles (frame.yml,page.yml,browserContext.yml, ...) plus aMANIFESTof their hashes and theDRIVER_VERSIONthey came from. A bump refreshes it withcargo xtask sync-protocol-specand the resultinggit diff protocol-specis the review;--checkruns offline in CI and pre-commit so the vendored copy cannot lag the pinned driver. This matters because the driver's validator drops parameters it does not recognize instead of rejecting them: 1.63 renamed everytracingStartcapture parameter and the traces came out empty, silently, with the whole suite still green. The companion gate,cargo xtask verify-protocol-methods, checks every method name the crate sends against the spec'scommands:blocks: the driver rejects an unknown method, and the caller usually sees a timeout rather than an error.resolveLocatorHandler,pathandconnectToServereach shipped that way for several releases.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

