CLAUDE.md
September 15, 2026 · View on GitHub
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/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 above
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.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. It gates the Claude Code plugin, whose default discovery
reads skills/ only, but the Agent Skills CLI scans .claude/skills/
too 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.
- 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-rustor as a Claude Code plugin.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.The same skill is also installable as a Claude Code plugin:
.claude-plugin/marketplace.jsonmakes this repo a marketplace. Default discovery picks upskills/and nothing else, so the contributor-facing skills in.claude/skills/stay in-repo without the manifest naming paths. Editing the skill means bumpingversionin.claude-plugin/plugin.json— that version is the update gate, and until it moves/plugin updatetells installed consumers they are already current. A pre-commit hook (scripts/check-plugin-version-bumped.sh) enforces it, and thePlugin version guardjob intest.ymlruns the same script against the PR base or pre-push tip, so a hookless clone cannot slip past. Validate manifest changes withclaude plugin validate ..
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)
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
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-files
Claude 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; and the configd Mach lookup lets
cargo vet/audit/deny run sandboxed. 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 hook
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 args
CI 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).
--in-diff narrows from there.
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 files (frame.yml,page.yml,browserContext.yml, ...). 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.