Thank you for helping make seleniumbase-rs a powerful, idiomatic Rust
browser-automation framework. Whether you are fixing a bug, adding a new stealth
evasion, writing a tutorial, or improving the MCP server, this guide will get
you oriented quickly.
Clone, set up Rust 1.99, build, and run the checks, including the real-Chrome tests.
Logs, headed runs, evidence when a test fails, and a mocked browser.
Run the harnesses in benches/ and compare a change fairly.
Features, binaries, the Dockerfile and build-time tips.
Table of Contents#
- Code of Conduct
- Quick Start
- Development Environment
- Project Layout
- Architecture at a Glance
- Code Style
- Error Handling
- Tracing and Observability
- Testing
- Parity with Python SeleniumBase
- Adding a Stealth Evasion Provider
- Adding a Fingerprint Setting or Policy Field
- Adding a Macro
- Adding an MCP Tool
- Adding an
sbrsSubcommand - Adding a Profile Payload Field
- Binary Patching
- Writing Documentation
- Working with AI Assistants
- Opening a Pull Request
- Publishing
- Licensing
Code of Conduct#
- Be respectful and constructive.
- Assume good intent.
- Focus feedback on code and design, not individuals.
- Do not include third-party anti-detect product names, trademarks, or proprietary payload examples in code, docs, or tests. Use generic terminology such as “external profile”, “anti-detect payload”, or “browser fingerprint”.
Quick Start#
Features, binaries, the Dockerfile and build-time tips are in docs/building.md. Every step from a fresh machine to a passing check is in Build from source.
Read CLAUDE.md once even if you are not using an AI assistant: it is a short map
of the modules, the commands and the things that have bitten before.
Three environment flags keep builds cheap on a small machine, and they let several checkouts share compiled dependencies:
Do not run two heavy cargo commands at once in the same target directory (cargo
serialises them with a lock), and do not share one CARGO_TARGET_DIR between
several checkouts of this repository: they overwrite each other’s compiled
library and doctests then fail with “extern location does not exist”.
Development Environment#
With mise, mise install sets up the tools below from
mise.toml (Rust 1.99 with clippy and rustfmt, Python 3.14 for the parity
scripts, Node for the Tauri example, mdBook, just and cargo-deny), and
mise run gate runs the checks a change has to pass; mise tasks lists the
rest (chrome, parity-docs, docs, smoke).
Required#
- Rust 1.99 or newer (stable channel);
rust-versioninCargo.tomlis the newest stable release and themsrvMinimum supported Rust version: the CI job that checks the crate with exactly the declared rust-version. CI job enforces it. cargo,rustfmtandclippy.- Python 3.10+ only for the scripts in
tools/(the parity tables). The crate itself, every feature included, needs no Python.
Optional but recommended#
- mdBook for documentation:
cargo install mdbook. - cargo-nextest for faster test runs
(
cargo install cargo-nextest; it does not run doctests, so also runcargo test --doc)..config/nextest.tomlhas a one-threadchromegroup for the tests that need a real Chrome. cargo-denyandcargo-auditfor the dependency checks, andjustfor the recipes in thejustfile.uvto regenerate the parity docs (just parity-docs).- A Chrome (and, for the WebDriver engine, a matching chromedriver:
sbrs install) for the tests marked#[ignore]. libwebkit2gtk-4.1-dev(Linux) or Xcode (macOS) if you build the Tauri example.
Recommended for working with an AI assistant#
brew install aovestdipaperino/tap/tokensave, then tokensave init in the
checkout; tokensave init keeps .tokensave/ out of git by adding it to
.git/info/exclude. A code graph of the repository
that an assistant queries over MCP (tokensave serve): symbols by name,
callers and callees, the impact of a change, and files read by symbol. The
assistant reads far less of the code to answer a question or make a change.
brew install rtk. A command proxy that condenses the output of cargo,
git, test runners and similar tools before it reaches an assistant
(rtk cargo test, rtk git log), so long build and test logs do not fill its
context. Exit codes are unchanged.
Neither is needed to build, test or contribute. How to set them up, with a prompt that runs the checks, is in Build from source.
Project Layout#
seleniumbase-rs
src
crates
cliPackage seleniumbase-rs-cli: the sbrs and sbrs-mcp binaries
Architecture at a Glance#
seleniumbase-rs is organized in layers:
- Two engines — the WebDriver engine (
BrowserConfig,BrowserSessionand theBaseCasestate machine overthirtyfour) and the pure CDP engine (sb_cdp), which talks to Chrome directly with no driver. They share the capability traits where it makes sense, not a common base class. - API layer — Capability traits (
BrowserApi,ElementApi,AssertionApi,ScreenshotApi) and thebase_case_implsmodules. New helpers should be added to the smallest applicable module and then exposed through the trait orBaseCaseimpl. - Stealth layer —
Fingerprint(oneSpoofper setting) +StealthPolicydescribe what to spoof;EvasionProviders generate the JavaScript bootstrap;ChromedriverPatchermutates the local driver binary;StealthReactorintercepts network requests via CDP. - Tooling layer —
sbrsCLI, the MCP server, the behave runner, and the Python importer.
When adding a feature, try to keep each layer independent. For example, a new
fingerprint provider should not depend on BaseCase, and a new CLI command
should not contain browser logic directly.
Code Style#
- Follow the Rust style enforced by
cargo fmt. - Keep Clippy clean with
-D warnings(command in Quick Start).src/mcpandsrc/sb_cdpare held to the stricter pedantic lint set. An#[allow(...)]needs areason = "...", and a function over 100 lines fails the lint, so split it. - Prefer explicit types on public APIs; this helps Copilot and other agents infer intent.
- Use
tracingfor logs instead ofprintln!in library code. - Avoid
unsafeunless absolutely necessary and clearly documented. - Keep functions small and focused. If a
BaseCasehelper grows beyond ~50 lines, add it to the appropriatebase_case_implsmodule. - Prefer
?andcrate::Result<T>over manualmatchpropagation. - Use
thiserrorfor new error variants and keep variants structured (avoid generic string payloads).
Error Handling#
seleniumbase-rs uses a single SeleniumBaseError enum in src/error.rs.
- Add a new variant when a failure mode needs special handling, a unique hint, or stable down-stream matching.
- Use helper constructors such as
SeleniumBaseError::element_not_foundinstead of formatting strings by hand. - Mark transient failures (network, driver not ready) by returning
truefromis_transient(). - Provide actionable hints via
hint(). - Instrument error paths with
tracing::error!and include context withResultExt::sb_context():
See docs/ABI_API.md and docs/help/common_problems.md for the error design
philosophy.
Tracing and Observability#
The crate uses tracing throughout. Common patterns:
Feature flags control subscriber output:
| Feature | Effect |
|---|---|
full-tracing | Enables detailed span lifecycle logging. |
error-backtrace | Declared for backtraces on SeleniumBaseError; no code reads it yet. |
Run the CLI with tracing enabled:
See docs/tutorials/tracing.md for subscriber configuration examples. How to
turn the logs on, and the other ways to find out why a run fails, are in
Debugging.
Testing#
- Write the test first when you can: the signature with a
todo!()body, the tests, then the code. A bug fix gets a test that fails without the fix. - Add unit tests for pure logic: selectors, fingerprint generation, macros, providers, JSON parsing.
- Use
#[tokio::test]for async helpers that do not require a browser. The pure CDP engine has a mock browser (Browser::new_mocked,MockCtrl), so waits, retries and error paths can be tested without Chrome (seetests/sb_cdp_*.rs). - Tests that need a real Chrome are named
*_chrome.rs(plusbrowser_smoke) and are#[ignore]d. Run them one test binary at a time:cargo test --test mcp_cdp_chrome -- --ignored. The tests that run thesbrsbinaries are incrates/cli/tests; name the package for those:cargo test -p seleniumbase-rs-cli --test cli_e2e_chrome -- --ignored. - A test that fakes
javaor another executable must run the script once (--warm) before timing it: macOS can take seconds to run a freshly written script the first time. - When writing doctests, mark browser-dependent snippets with
no_runorignore. Tutorials indocs/tutorials/are not compiled, so keep their code true by hand.
Verification matrix (run before every PR)#
Count failures from the exit code and the test result: lines, not from a
filtered search of the output. The repository is a workspace of two packages, the
library seleniumbase-rs and the command line seleniumbase-rs-cli
(crates/cli), and the commands above cover both (default-members). Use
-p seleniumbase-rs for the library alone, with its own default features: the
command line turns pdf and mcp-server on, and a build of both unifies them.
A change that touches a Cargo.toml must keep the Dockerfile skeleton step in
step: a new [[bin]], [[bench]], [[example]] or [[test]], in either
manifest, must be listed there, and tests/dockerfile_skeleton.rs fails if it
is not. tests/library_manifest.rs fails if a binary or a command-line crate
(usage, or the clap crates it replaced) comes back into the library package.
cargo deny check gates advisories, licenses, banned crates, and dependency
sources against deny.toml. Every advisory that is ignored there carries a
written rationale, so if a new one appears, either upgrade the dependency or
add an entry explaining why the risk is acceptable.
Do not widen the ignore list silently.
The crate declares rust-version = "1.99", the newest stable release when it was
last raised, which the msrv CI job enforces with that exact toolchain. The
floor is a choice to track current stable, not something the crate’s own source
needs, so when it moves, bump rust-version, the msrv job, and the
Dockerfile base image together.
On macOS, if a test binary cannot load libpython, prefix the test commands with:
Parity with Python SeleniumBase#
The project’s goal is that every API of Python SeleniumBase 4.55.2 (and of its MCP server) exists here or has a stated reason why not.
parity/api.tomlrecords one entry per upstream API with astatus:implemented(Rust code and a test),composed(existing Rust calls already do it; name them inrust),not-applicable(with areasonspecific to that API) orplanned. Never mark somethingimplementedwithout code and a test;tests/parity.rschecks that every Rust path in the manifest exists.- After editing the manifest, regenerate
docs/parity.mdwithjust parity-docs(oruv run tools/parity/render_docs.py) and commit both.just parity-checkverifies them. parity/upstream.tomlanddocs/UPSTREAM_SYNC.mddescribe how to move to a newer upstream release.
Adding a Stealth Evasion Provider#
The stealth layer uses a provider registry. evasions::bootstrap_script(fp) is
a thin wrapper over the registry.
Implement EvasionProvider
Open src/stealth/providers/builtin.rs and implement EvasionProvider:
Register it
Register it in all() at the bottom of builtin.rs (keep sorted by priority).
Wrap replaced natives
Wrap replaced natives so Function.prototype.toString still reports
[native code]:
The native_to_string provider (priority 5) installs window.__sbNative
before any other provider runs.
Seed the noise
Use ctx.seed for deterministic per-session noise (canvas, audio, WebGL).
Test it
Add a unit test asserting the snippet contains expected markers and that
applies is gated correctly.
Document it
Update docs/tutorials/fingerprint_stealth.md.
Adding a Fingerprint Setting or Policy Field#
A setting is what the browser claims (one Spoof<T> field of Fingerprint); a
policy field is how the claims are delivered (a field of StealthPolicy in
src/stealth/policy.rs).
For a setting:
Add the field
Add the field to Fields in src/stealth/fingerprint.rs, with its default
in impl Default for Fields and #[serde(skip_serializing_if = "Spoof::is_natural")],
and a read accessor in the accessors! list of Fingerprint. If its value is
not a plain type, add the value type to src/stealth/settings.rs with a
conversion from the obvious tuple.
Add the setter
Add its setter to the setters! list of FingerprintBuilder, and its name to
has_random and without_random. Decide whether it has an off state: if not,
add it to refuse_useless_off; and what Random draws: add the arm to
draw_random (taking the value from the Draw in randomize.rs), or make it
an error there.
Give it a dimension
Give the dimension it belongs to (or a new Dimension variant, which is
#[non_exhaustive]) an arm in Fingerprint::is_masked. Providers, launch
arguments and overrides ask is_masked, never a field by hand.
Validate it
Add coherence validation in Fingerprint::validate() if the setting has
required companions.
Wire it in
Wire the setting into the launcher (browser/session.rs), provider scripts,
or the reactor if applicable. A provider defines only a setting that is a
Value, and handles Off where it has one.
Read it from a payload
Add the payload key to ProfileParams::fingerprint_builder if a payload can
carry it, reading the common words of Word.
Document it
Document the setting in docs/tutorials/fingerprint_stealth.md.
For a policy field, add the field, its default and its preset values to
StealthPolicy, a method of the same name, and a builder shortcut
(with_policy); a preset never changes a setting.
Adding a Macro#
Public macros live in src/macros.rs and are re-exported at the crate root.
Add the macro
Add the macro with #[macro_export].
Test it
Write a compile-time unit test under the definition.
Document it
Update docs/tutorials/macros.md and the macro table in README.md.
Adding an MCP Tool#
The MCP server (src/mcp/, started by crates/cli/src/mcp_server.rs) uses the
rmcp crate.
A tool is a ToolDef in src/mcp/; read src/mcp/devtools.rs for working
examples (they are generic over the session type, so one definition serves both
engines) and src/mcp/stealth.rs for how an extra toolset is attached.
Declare it
Declare it with ToolDef::new(name, title, description, effect, schema, handler). The handler is an async fn(Ctx<S>, Args) -> Result<Output, ToolError>. Pick the Effect honestly: Observe reads the open web,
Inspect looks at this machine, Act changes the page, Set changes the
session idempotently, Overwrite writes local files.
Report failures
Return ToolError::Failed(SeleniumBaseError) for failures: the server turns it
into a failure report with the page’s state, the hint and the next tools to
try. Files a tool writes go through the output directory
(SB_MCP_OUTPUT_DIR), never to an arbitrary path.
Register it
Register it in the server that offers it (cdp.rs, driver.rs, sb.rs).
Test it
Test it without Chrome first (tests/mcp_cdp.rs, tests/mcp_devtools.rs),
then with a real Chrome in a *_chrome.rs test if it reads a page.
Document it
Document it in docs/tutorials/mcp_server.md, add it to CHANGELOG.md, and
give it an entry in parity/api.toml if upstream has the same tool.
Adding an sbrs Subcommand#
Add the variant
Add the new variant to Commands in crates/cli/src/sbrs.rs with an
idiomatic /// description (its first paragraph is the short help).
Parse the arguments
Declare its arguments and flags with #[usage(...)] attributes on the fields, as
the neighbouring variants do (long, short, default, conflicts(...),
value_enum; a switch takes bool_value).
Handle it
Add a handler branch in async fn run.
Clean up
If the command launches a browser, prefer creating a BaseCase through
run_browser_test() so cleanup is deterministic.
Write the generated files
Write completions/sbrs.usage.kdl and docs/help/cli_reference.md again with
SB_UPDATE_COMPLETIONS=1 cargo test -p seleniumbase-rs-cli --bin sbrs usage_spec;
the same test fails in CI when they are stale.
Document it
Update docs/help/commander.md and README.md if the command is user-facing.
Adding a Profile Payload Field#
External profile payloads are parsed in src/profile_payloads/profile.rs.
Add the field
Add the field to ProfileParams (or the appropriate nested struct).
Map it
Map it to BrowserConfig or Fingerprint in to_browser_config() and
browser().
Test it
Add a unit test for parsing and round-tripping.
Document it
Update docs/tutorials/browser_profiles.md.
Binary Patching#
ChromedriverPatcher modifies a local chromedriver binary to help evade
binary-signature detection.
- Patches are defined as
EnginePatchstructs insrc/stealth/patcher.rs. - Each patch has a human-readable
id, byte sequence search/replace, and optional marker detection. - Add new patches to
ChromedriverPatcher::default_patches(). - Always test patches against the driver versions listed in
docs/help/binary_patching.md. - Backups are stored next to the binary with
.sb-backupextension.
Writing Documentation#
- All public items should have doc comments (
///). - Use examples in doc comments; they run as doctests.
- User-facing tutorials go in
docs/tutorials/. - Reference/help pages go in
docs/help/. - AI-agent guidance goes in
COPILOT.mdandCLAUDE.md. - After adding a page, add it to
docs/SUMMARY.md. - A user-visible change also gets an entry in
CHANGELOG.md. - Say what was verified and how. A claim like “works in Chrome” needs a real Chrome behind it; otherwise say it was checked against the mock or by reading.
- Build the book locally before pushing:
mdbook build.
Working with AI Assistants#
Assistants are welcome, and the repository is set up for them: AGENTS.md is the
short rule sheet any agent can read, llms.txt indexes the docs for models,
CLAUDE.md is the module map and rule list, rust-analyzer.toml makes analysis cover the
optional features, and docs/tutorials/vibe_coding.md shows how to use the MCP
servers to explore a page and export a test. Whoever opens the pull request owns
the result:
- Run the checks yourself and report their exit codes; do not paste “it should pass”.
- Read the diff. Reject tests that cannot fail, assertions that were loosened to
make a test pass, and
sleepcalls added instead of waits. - Do not let an assistant mark a parity entry
implementedwithout code and a test, or invent an API: check it against the code or the library’s docs. - Keep secrets out of prompts, logs and committed files.
Opening a Pull Request#
Branch
Branch from main unless told otherwise.
Commit
Keep commits focused and atomic, with conventional-commit subjects
(feat(scope): ..., fix(scope): ..., test: ..., docs: ..., refactor: ...).
Commits carry no co-author or tool trailers, and never contain secrets.
Update tests and docs
Update the tests, CHANGELOG.md and the docs in the same change.
Describe the change
Fill out the PR description with:
- What changed and why.
- Verification commands that passed.
- Any breaking API changes and migration notes.
Check CI
Ensure CI is green.
PR review checklist#
-
cargo fmt --all -- --checkis clean. -
cargo clippy --all-targets --features "mcp-server test-util" -- -D warningsis clean. -
cargo test --features "mcp-server test-util"passes. -
cargo deny checkandcargo auditare clean ifCargo.lockchanged. - A new
[[bin]],[[bench]],[[example]]or[[test]]is in theDockerfileskeleton. -
parity/api.tomlanddocs/parity.mdare updated for a new API. -
mdbook buildsucceeds if docs changed. - New public items have doc comments.
- New features are covered by tests or examples.
- Documentation reflects the change.
- No third-party anti-detect product names or trademarks were added.
Publishing#
The crate is not published to crates.io; it is distributed from the repository.
To publish a change to main:
Verify the exact commit
Run the full verification matrix on the exact commit you will push.
Fetch
git fetch, and confirm origin/main has not moved since you started.
Fast-forward push
Push as a fast-forward only (for example
git push origin <commit>:refs/heads/main), then check that your local
branch and origin/main are the same commit.
Never force-push.
A docs-only commit on top of a commit that passed the checks may be pushed after it, and its message should say so.
Licensing#
By contributing, you agree that your contributions will be licensed under the same license as the project (MIT).