browser-use/browser-use
github.com/browser-use/browser-use ↗ · 508 files · 4.3 MB
--- Navigate the web like a human does. Find an available slot, pick a date and time, handle the CAPTCHA, and book a driving test.
Overview
browser-use 0.13.10 is an async Python library (and a thin CLI) that lets an LLM complete web tasks by driving Chromium over the Chrome DevTools Protocol. The library loop is Agent.run: snapshot the DOM and a screenshot, ask a chat model for the next actions, execute those actions through an event bus and watchdog services, and repeat until a done action or a step/failure cap. It is aimed at developers embedding a browser agent and at coding agents that drive a real browser through the CLI; the distinctive pieces are the indexed DOM the model clicks, a large built-in action registry, and first-class hooks into Browser Use Cloud browsers, the BU LLM gateway, skills, and a remote @sandbox runner.
System map
The installable package is browser_use, built with hatchling and exposed as the console scripts browser-use, browseruse, bu, and browser (browser_use.cli:main), plus python -m browser_use. The runtime splits into an Agent orchestrator, a BrowserSession that owns a bubus event bus and a cdp-use CDP client, a Tools/Registry action layer, and a DomService that serializes the page for the model. Persistence is local (temp agent directories, cookie storage state, config under ~/.config/browseruse) plus optional cloud event sync. External systems are the Browser Use cloud browser API, the LLM gateway, the sandbox stream API, PostHog telemetry, and optional provider SDKs (OpenAI, Anthropic, Google, Groq, Ollama, AWS, MCP). CI is GitHub Actions: per-file pytest of tests/ci, ruff/pre-commit/pyright, PyPI publish on GitHub release, and a multi-arch Docker image.
- pyproject.toml names the package browser-use version 0.13.10, requires Python >=3.11,<4.0, and registers console scripts browser-use, browseruse, bu, and browser to browser_use.cli:main, with the deprecated browser-use-tui alias.
- The public import surface is lazy: browser_use/__init__.py __getattr__ loads Agent, Tools, Controller, DomService, Chat* model classes, and sandbox on first access, and maps the name Browser to BrowserSession.
- python -m browser_use calls browser_use.cli.main and exits with its return code.
- Agent in browser_use/agent/service.py is the task orchestrator. BrowserSession in browser_use/browser/session.py owns the CDP connection, tab targets, and a bubus event bus; start() is handled by on_BrowserStartEvent, which attaches watchdogs before launching or connecting.
- BrowserSession.attach_all_watchdogs constructs Downloads, StorageState (conditional), LocalBrowser, Security, AboutBlank, Popups, Permissions, DefaultAction, Screenshot, DOM, Recording, optional HAR, and optional Captcha watchdogs. CrashWatchdog registration in that method is commented out.
- Local Chrome is started by LocalBrowserWatchdog.on_BrowserLaunchEvent → _launch_browser, which returns a psutil.Process and a CDP URL. Cloud browsers are created when BrowserProfile.use_cloud or cloud_browser_params is set, via CloudBrowserClient.create_browser POST {api}/api/v2/browsers using BROWSER_USE_API_KEY or CloudAuthConfig.
- Configuration is env-driven. OldConfig and FlatEnvConfig in browser_use/config.py read BROWSER_USE_LOGGING_LEVEL, ANONYMIZED_TELEMETRY, BROWSER_USE_CLOUD_SYNC, BROWSER_USE_CLOUD_API_URL (default https://api.browser-use.com), LLM keys, and proxy vars. Config, profiles, and extensions live under BROWSER_USE_CONFIG_DIR, defaulting to ~/.config/browseruse.
- If Agent is constructed with llm=None and DEFAULT_LLM is empty, it instantiates ChatBrowserUse(). get_llm_by_name in browser_use/llm/models.py is the string factory for openai_, azure_, google_, anthropic_, mistral_, cerebras_, and bu_ names and refuses OCI (use ChatOCIRaw directly).
- ChatBrowserUse posts to BROWSER_USE_LLM_URL or https://llm.api.browser-use.com, default model bu-2-0, and requires BROWSER_USE_API_KEY. The @sandbox decorator posts cloudpickled user code to https://sandbox.api.browser-use.com/sandbox-stream.
- CI test workflow .github/workflows/test.yaml discovers every tests/ci/**/test_*.py and runs them as a matrix on ubuntu with uv, IN_DOCKER=true, and ANONYMIZED_TELEMETRY=false. lint.yml runs ruff PLE, pre-commit, and pyright. publish.yml uploads to PyPI on GitHub release. docker.yml builds and publishes the image. The root Dockerfile is python:3.12-slim with IN_DOCKER=True.
- DomService.get_dom_tree / get_serialized_dom_tree is the DOM boundary the agent consumes. FileSystem and ScreenshotService are rooted at a per-run temp directory tempfile.gettempdir()/browser_use_agent_{id}_{timestamp} unless file_system_path is passed.
Feature catalog
User-facing capability sits on three paths described in the README: a hosted cloud product (outside this process), a CLI that executes Python against a browser daemon, and the Python Agent library. The library's default tool registry covers navigation, element interaction, extraction, tabs, files, and task completion. Optional features include cloud browsers, the remote sandbox, MCP server mode, Gmail OTP actions, cloud skills, structured output, vision, planning, a judge pass, GIF/HAR/video recording, and domain or captcha policy.
- README.md defines three products: fully hosted cloud, the CLI (Path 2, aimed at coding agents), and the Python library (Path 3: Agent + ChatBrowserUse or ChatOpenAI, optional Browser(use_cloud=True)).
- Tools.__init__ registers default actions search (DuckDuckGo/Google/Bing), navigate, go_back, wait (capped at 30s), click/input and related element actions, upload_file, dropdown_options, select_dropdown, write_file, replace_file, read_file, evaluate, and done. Tools.exclude_action and the exclude_actions constructor argument remove actions from the registry.
- search and navigate dispatch NavigateToUrlEvent and are marked terminates_sequence=True so multi_act drops queued actions after them. navigate retries once when the post-navigation DOM looks empty.
- The CLI dispatcher handles --mcp, --cli-mcp, install, init/--template, and skill locally. Legacy subcommands in _LEGACY_HINTS (open, state, screenshot, eval, cookies, python, run, connect, cloud, daemon, record, mcp, --headed, --cdp-url, and others) print a 3.0 migration message and return exit code 2. Anything else is delegated to browser_harness.run.main, which executes Python from argv or stdin in a persistent browser session.
- browser-use skill install copies the packaged browser-use skill into assistant skill directories for agents, claude, codex, copilot, cursor, gemini, openclaw, and opencode via TARGET_DIR_BUILDERS in browser_use/skills/install.py.
- MCP server mode (uvx browser-use --mcp) exposes tools browser_navigate, browser_click, browser_type, browser_get_state, browser_extract_content, browser_get_html, browser_screenshot, browser_scroll, browser_go_back, browser_list_tabs, browser_switch_tab, browser_close_tab, retry_with_browser_use_agent, browser_list_sessions, browser_close_session, and browser_close_all. Logging is forced to stderr so it does not corrupt JSON-RPC.
- Browser(use_cloud=True) provisions a remote browser. BrowserProfile also exposes cloud_profile-style CreateBrowserRequest params, cdp_url for an existing browser, proxy, allowed_domains, prohibited_domains, block_ip_addresses, captcha_solver (default True), and demo_mode.
- @sandbox() requires the wrapped coroutine to take a browser parameter, serializes the function source and closure with cloudpickle, and streams SSE events from the sandbox. Callbacks include on_browser_created, on_log, on_result, and on_error. cloud_profile_id, cloud_proxy_country_code, and cloud_timeout are forwarded in the payload.
- Agent accepts skill_ids (or skills) and registers them as actions through SkillService, which uses AsyncBrowserUse from browser-use-sdk and lists enabled finished skills. Wildcard '*' loads at most the first 100 skills.
- register_gmail_actions adds get_recent_emails (keyword plus max_results 1-50) for OTP and magic-link retrieval via GmailService. Registry._replace_sensitive_data also expands TOTP secrets with pyotp.TOTP when a placeholder refers to a bu_2fa_code secret.
- Structured task output is output_model_schema on Agent, which switches the done action to StructuredOutputAction. SystemPrompt selects among system_prompt.md, flash variants, Anthropic variants, and browser-use-specific markdown templates based on provider, flash_mode, and use_thinking. ChatBrowserUse forces flash_mode=True and therefore enable_planning=False.
- Other user-visible Agent knobs implemented in __init__ include use_vision, generate_gif, sensitive_data, initial_actions, fallback_llm, use_judge, directly_open_url, message_compaction, loop_detection_enabled, and demo_mode. pause, resume, and stop mutate AgentState and are wired to SignalHandler during run.
Data flow
A task string enters Agent.__init__, which picks an LLM, builds a system prompt, a MessageManager, a FileSystem, and a BrowserSession. Each step pulls a BrowserStateSummary (DOM text, selector map, screenshot) from the browser, redacts secrets, and sends the message list to the LLM. The model returns an AgentOutput whose action list is validated by Pydantic and executed one by one; ActionResult objects are written back into history and into the next prompt. Side effects leave the process through CDP commands, local files under the agent temp dir, optional conversation and GIF dumps, PostHog, and authenticated POSTs to the Browser Use events API.
- Agent.run emits CreateAgentSessionEvent once and CreateAgentTaskEvent at start, then BrowserSession.start, _register_skills_as_actions, and _execute_initial_actions before the step loop. On the way out it records token usage, dispatches UpdateAgentTaskEvent, optionally writes a GIF, and calls close().
- step() order is: optional captcha wait, _prepare_context, _get_next_action, _execute_actions, _post_process, and _finalize in a finally block. _prepare_context always requests get_browser_state_summary(include_screenshot=True), refreshes page-scoped actions, and calls MessageManager.create_state_messages.
- _get_next_action wraps _get_model_output_with_retry in asyncio.wait_for using settings.llm_timeout (auto-chosen from the model name when the caller passes None: 30s groq, 75s default, 90s for o3/claude/deepseek/gemini-3-pro). The parsed AgentOutput is stored on state.last_model_output.
- multi_act walks the action list. Tools.act applies a per-action timeout (argument, or BROWSER_USE_ACTION_TIMEOUT_S, default 180s) and calls Registry.execute_action. The registry builds the action's Pydantic param model, substitutes <secret> placeholders, and injects browser_session, page_url, cdp_client, file_system, page_extraction_llm, available_file_paths, and extraction_schema by parameter name.
- Browser actions do not call CDP directly from the tool in the navigation case: they dispatch bubus events (NavigateToUrlEvent, GoBackEvent, Click, Type, and others). BrowserSession.on_NavigateToUrlEvent performs the CDP navigation; DefaultActionWatchdog handles the element-level events after attach_all_watchdogs.
- DOM state is produced by DomService.get_dom_tree and get_serialized_dom_tree (accessibility tree plus DOM snapshot, visibility and iframe filtering) and cached on the session as a selector map keyed by integer index. Tools resolve click and dropdown targets with browser_session.get_element_by_index.
- MessageManager._filter_sensitive_data redacts secret values out of prompts via redact_sensitive_string. At execution time, domain-keyed sensitive_data dicts are applied only when match_url_with_domain_pattern matches the current URL; a flat {key: value} dict is applied on every domain.
- File tools write under FileSystem.data_dir, which is base_dir/browseruse_agent_data. The default base_dir is the per-agent temp directory. _resolve_filename strips directories with basename to block traversal. External reads happen only when read_file is called with a name that is an exact member of available_file_paths, and then the path is opened as given.
- Downloads discovered by the agent are merged into available_file_paths inside _check_and_update_downloads, which runs after the browser-state fetch and again after actions. upload_file refuses paths that are not in that list and not a fresh download.
- When BROWSER_USE_CLOUD_SYNC is on and DeviceAuthClient is authenticated, CloudSync.handle_event POSTs each event as JSON to {BROWSER_USE_CLOUD_API_URL}/api/v1/events with a 10s timeout and swallows HTTP failures. Unauthenticated runs skip the send unless allow_session_events_for_auth is set.
- ChatBrowserUse.ainvoke serializes messages, optional output_format JSON schema, request_type (browser_agent or judge), anonymized_telemetry, and session_id, then retries HTTP 429/500/502/503/504 with exponential backoff. Observability spans are optional Laminar decorators from browser_use/observability.py and no-op if lmnr is absent.
- save_history writes AgentHistory JSON through history.save_to_file with sensitive_data filtering. If save_conversation_path is set, each step's messages are appended via save_conversation. generate_gif calls create_history_gif and emits CreateAgentOutputFileEvent when the file exists. Cookie persistence is StorageStateWatchdog._save_storage_state, enabled only when storage_state or user_data_dir is set, defaulting to save on shutdown (auto_save_interval 60s, save_on_change False).
Behavior traces
Three flows dominate. Startup brings up logging, the agent object, the browser process or cloud session, and the watchdog set before any model call. The primary step is a fixed phase sequence from DOM snapshot through LLM output to event-bus actions. Persistence is split across the in-memory AgentHistoryList, the on-disk file system and screenshots, optional conversation and GIF files, cookie storage state, and cloud event sync.
- Process startup for the library: importing browser_use runs setup_logging unless BROWSER_USE_SETUP_LOGGING=false, reading CONFIG log file paths, and monkeypatches asyncio BaseSubprocessTransport.__del__ to ignore 'Event loop is closed'.
- Agent construction order after argument validation: resolve llm (DEFAULT_LLM via get_llm_by_name, else ChatBrowserUse), force flash_mode for provider browser-use, build BrowserSession if none was passed, assign task_id and session_id via uuid7str, create AgentState and AgentHistoryList, create the temp agent directory, then _set_file_system and _set_screenshot_service.
- Browser startup order inside on_BrowserStartEvent: attach_all_watchdogs; if no cdp_url and use_cloud, CloudBrowserClient.create_browser and store cdpUrl with is_local False; else if local, dispatch BrowserLaunchEvent and read BrowserLaunchResult.cdp_url from LocalBrowserWatchdog; then connect() under _connection_lock with a 15s timeout; then dispatch BrowserConnectedEvent so storage restore finishes before start returns.
- SecurityWatchdog.on_NavigateToUrlEvent runs before navigation and raises ValueError when _is_url_allowed is false. on_NavigationCompleteEvent sends blocked redirects to about:blank. on_TabCreatedEvent tries to close a disallowed new tab. block_ip_addresses uses ipaddress plus socket.inet_aton so non-standard IPv4 spellings cannot bypass the check. Empty allow and deny lists allow all URLs.
- Primary step trace: step → wait_if_captcha_solving → _prepare_context → get_browser_state_summary → _check_and_update_downloads → _update_action_models_for_page → registry.get_prompt_description → MessageManager.prepare_step_state / maybe_compact_messages / create_state_messages → budget, replan, exploration, and loop nudges → _get_next_action → multi_act → _post_process → _finalize.
- Inside multi_act, each action is executed through Tools.act. Actions registered with terminates_sequence=True (navigate, search, go_back) abort the rest of the queue, and a URL or target change after any action also aborts the remainder so later clicks are not applied to a stale DOM.
- Initial URL handling: when directly_open_url is true, _execute_initial_actions runs the converted initial action list through multi_act before the LLM loop and stores that as history step 0 with a synthetic AgentOutput. The call is wrapped in wait_for(step_timeout) so a dead CDP socket cannot hang startup forever.
- Done path: the done action returns ActionResult(is_done=True). _post_process logs the final result. _execute_step sees history.is_done(), optionally _judge_and_log, invokes register_done_callback, and run() breaks. On consecutive_failures past max_failures (plus one if final_response_after_failure) the loop stops without a successful done.
- CLI startup trace: main → _dispatch. No args and a TTY prints _QUICKSTART and returns 0. Piped stdin is reassigned to a StringIO and passed to _run_browser_harness, which calls browser_harness.run.main. --mcp imports and runs the MCP stdio server. A NameError raised from executed <string> code is rewritten into an unknown-helper message and exit 2.
- Cookie persistence trace: BrowserConnectedEvent triggers StorageStateWatchdog.on_BrowserConnectedEvent → _load_storage_state. On BrowserStopEvent / SaveStorageStateEvent it calls _save_storage_state. The watchdog is not attached at all unless storage_state or user_data_dir is configured.
- Cloud sync auth is a device-code OAuth flow in DeviceAuthClient: POST /api/v1/oauth/device/authorize with scope 'read write', then poll /api/v1/oauth/device/token with grant_type urn:ietf:params:oauth:grant-type:device_code, storing access_token on CloudAuthConfig.
- Rerun trace: load_and_rerun can substitute detected variables via _substitute_variables_in_history, then rerun_history walks stored actions, using detect_variables_in_history (element id, name, type, placeholder, aria-label, then value patterns) to name inputs. save_history dumps the filtered history to AgentHistory.json by default.
Testing signals
The project uses pytest 9 with pytest-asyncio in auto mode, a 300s default timeout, and xdist. The CI-gated suite is tests/ci, discovered as one GitHub Actions job per test file with a 4 minute job timeout. Tests are expected to use a real browser plus pytest-httpserver; the LLM is the only allowed mock, via conftest.create_mock_llm. Large areas outside tests/ci (examples, most LLM live tests, the beta agent, and the browser_harness CLI daemon) are not part of that matrix. A separate eval workflow can POST the PR commit to an external eval platform for owner, member, or collaborator PRs.
- pytest is configured in pyproject.toml: timeout 300, asyncio_mode auto, testpaths tests, addopts -svx --strict-markers --tb=short --dist=loadscope, markers slow, integration, unit, and asyncio. bin/test.sh runs uv run pytest --numprocesses auto tests/ci.
- tests/ci is grouped into browser/, infrastructure/, interactions/, models/, security/, plus many top-level test_*.py files covering agent planning, tools, DOM, filesystem, MCP, sandbox structured output, variable substitution, and cloud events.
- tests/ci/conftest.py autouse fixture forces SKIP_LLM_API_KEY_VERIFICATION, ANONYMIZED_TELEMETRY=false, and placeholder cloud URLs so tests do not talk to production. create_mock_llm builds a BaseChatModel that returns scripted AgentOutput actions. The module docstring says tests must not connect to production services.
- CLAUDE.md requires real objects in tests, forbids mocking except the LLM, and forbids real remote URLs such as google.com; HTML fixtures should be served with pytest-httpserver. It also says files that pass move into tests/ci and that @pytest.mark.asyncio is unnecessary because asyncio_mode is auto.
- The GitHub Actions test workflow checks out the repo, caches Playwright Chromium, lists tests/ci test_*.py into a JSON matrix, and runs each file on ubuntu-latest with timeout-minutes 4. It injects OPENAI, ANTHROPIC, GOOGLE, GROQ, AZURE, BROWSER_USE, and OPENROUTER API keys from secrets when present.
- Security-focused tests live in tests/ci/security and include domain filtering, download filename sanitization, IP blocking, MCP allowed domains, security flags, sensitive data, and upload path containment.
- pyright include is browser_use, examples, and tests, but excludes tests/scripts, tests/old, browser_use/dom/playground, browser_use/llm/oci_raw, browser_use/llm/tests/test_chat_models.py, browser_use/llm/tests/test_single_step.py, and browser_use/beta/service.py. Those paths are not held to the type checker.
- Lint CI is three jobs: ruff check --select PLE, pre-commit run --all-files on Python 3.11, and pyright after uv sync --dev --all-extras. Dev dependencies pin pytest==9.0.2, pytest-asyncio==1.3.0, pytest-httpserver==1.1.3, pyright==1.1.408, and ruff==0.14.14.
- eval-on-pr.yml, for PRs whose author association is OWNER, MEMBER, or COLLABORATOR, POSTs the head SHA to secrets.EVAL_PLATFORM_URL/api/triggerInteractionTasksV6 with a default test case of InteractionTasks_v8. This is an external eval, not the local pytest suite.
- browser_use/llm/tests contains live-model tests (test_chat_models.py, test_single_step.py, provider cache and schema tests) that are not under tests/ci and are partly excluded from pyright, so they do not run in the per-file CI matrix.
- There is no tests/ci coverage of the browser_harness daemon itself; CLI tests that exist (tests/ci/test_browser_use_cli.py is in the tree) can only cover this repo's dispatcher. The harness package is an external dependency (browser-harness==0.1.13).
Risk map
The highest-risk surfaces are arbitrary page JavaScript, the remote sandbox that cloudpickles and ships user code, secret substitution, file upload and external file reads, and domain policy that is off unless configured. Several controls exist and are tested (domain filter, IP blocking, upload whitelist, sensitive-data redaction), but the JavaScript evaluate path only fixes quotes, the flat sensitive_data format is global, and CrashWatchdog is disabled. Docs in AGENTS.md disagree with Agent.__init__ defaults, which is a trap for agents editing call sites.
- evaluate sends the model-supplied string to CDP Runtime.evaluate with awaitPromise and returnByValue after _validate_and_fix_javascript, which only rewrites escaped quotes, regex escapes, and a few selector quote styles. There is no script allowlist or CSP wrapper.
- @sandbox base64-encodes cloudpickle.dumps of the closure and sends the reconstructed source plus pickled params to sandbox.api.browser-use.com with the API key in X-API-Key. That is remote code execution of whatever the decorated function closes over.
- Legacy sensitive_data entries that are plain strings (not nested under a domain pattern) are merged into applicable_secrets for every URL. Domain-scoped entries are filtered with match_url_with_domain_pattern. Missing placeholders are logged, not raised.
- read_file treats a string that is exactly in available_file_paths as an external file and opens that path with anyio.open_file, python-docx, or pypdf. The check is equality, not a resolved-path containment check. upload_file has the same membership test.
- FileSystem.__init__ deletes and recreates base_dir/browseruse_agent_data with shutil.rmtree when that subdirectory already exists. Passing a file_system_path that already contains a browseruse_agent_data directory wipes it.
- SecurityWatchdog allows every URL when both allowed_domains and prohibited_domains are unset. disable_security on BrowserProfile appends CHROME_DISABLE_SECURITY_ARGS to the Chrome launch flags. block_ip_addresses defaults to False.
- ProductTelemetry is on by default (ANONYMIZED_TELEMETRY defaults true) and sends events to https://eu.i.posthog.com with a project API key hardcoded in telemetry/service.py. The device id is persisted at ~/.config/browseruse/device_id or derived from MAC and hostname. Opt out with ANONYMIZED_TELEMETRY=false.
- Crash recovery is not wired: the CrashWatchdog import and event subscriptions inside attach_all_watchdogs are comments. Browser disconnects are handled instead by Agent._handle_step_error waiting on browser_session._reconnect_event.
- AGENTS.md documents Agent defaults that do not match the constructor: code uses max_steps=500 on run(), max_actions_per_step=5, max_failures=5, step_timeout=180, and use_vision=True. The checked-in agent guide still says max_steps 100, max_actions_per_step 3, max_failures 3, step_timeout 120, and use_vision 'auto'.
- browser_use/controller/__init__.py only re-exports Controller, which is an alias of Tools. browser_use/beta/service.py duplicates a large slice of Agent (it imports the same history, judge, and message-manager types) and is excluded from pyright as a dynamic beta wrapper.
- MCP mode inserts the package parent onto sys.path, disables logging below CRITICAL, and can run a full Agent (retry_with_browser_use_agent) inside the MCP client process. A compromised or overly broad MCP client therefore gets the same browser and filesystem powers as the library.
- Cloud browser and LLM calls send the API key in headers (X-Browser-Use-API-Key and the chat client's own auth). create_browser logs the live URL at info level. stop_browser PATCHes /api/v2/browsers/{id} with action stop.
Extension points
New behavior is meant to be added as registered actions, not by editing the step loop. Tools.action and Registry.action are the plugin table; special parameter names are the injection API. Browser behavior is extended by watchdogs that subscribe to bubus events. LLM providers plug in through BaseChatModel and get_llm_by_name. Cloud skills, Gmail, and MCP are additional registries on top of the same action or tool list. System prompts are selected markdown files, overridable with override_system_message or extend_system_message.
- Registry.action(description, param_model, domains/allowed_domains, terminates_sequence) is the registration decorator. It normalizes the function to keyword-only, builds a Pydantic param model from the signature when param_model is omitted, and rejects **kwargs. Tools.action is a pass-through to that decorator.
- Injected parameter names recognized by Registry._get_special_param_types are context, browser_session, page_url, cdp_client, page_extraction_llm, available_file_paths, has_sensitive_data, file_system, and extraction_schema. sensitive_data is added to the context only when the action name is input. The docs warn that the name must be browser_session, not browser.
- Registry.exclude_action and Tools.exclude_action remove a name from the action map and block re-registration. Tools.__getattr__ also exposes registered actions as direct methods for tests.
- Per-action domain limits are the domains (alias allowed_domains) argument on Registry.action, separate from BrowserProfile.allowed_domains enforced by SecurityWatchdog. Both can be set.
- New browser services are watchdogs: a BaseWatchdog subclass with LISTENS_TO and EMITS class vars, constructed and attach_to_session() inside BrowserSession.attach_all_watchdogs. SecurityWatchdog is the template (LISTENS_TO NavigateToUrlEvent, NavigationCompleteEvent, TabCreatedEvent; EMITS BrowserErrorEvent).
- LLM extensions implement BaseChatModel and are either imported lazily from browser_use/__init__.py _LAZY_IMPORTS or constructed by get_llm_by_name. Adding a provider requires both a chat module and a branch in that factory if string names should work.
- System prompt extension points are override_system_message (replaces the template) and extend_system_message (appended). Otherwise SystemPrompt._load_prompt_template picks a file under browser_use/agent/system_prompts based on is_browser_use_model, flash_mode, is_anthropic, and Anthropic 4.5 caching.
- Cloud skills plug in through Agent(skill_ids=...) and SkillService, which turns each API skill into a registry action in _register_skills_as_actions. Gmail plugs in through register_gmail_actions(tools). MCP tools are a separate list built in the MCP server, not the Registry.
- Coordinate clicking is a mode switch: Tools.set_coordinate_clicking re-registers the click action. File types are a dict on FileSystem._file_types (md, txt, json, jsonl, csv, pdf, docx, html, xml, png, gif, jpg, jpeg, webp); a new extension means a new BaseFile subclass in that map.
- Run hooks are on_step_start and on_step_end passed to Agent.run, plus register_new_step_callback, register_done_callback, register_should_stop_callback, and register_external_agent_status_raise_error_callback on the constructor. output_model_schema swaps the done schema via Tools.use_structured_output_action.
- CLI skill targets are the TARGET_DIR_BUILDERS dict. Adding an assistant is a new entry there, not a new command parser. Legacy CLI verbs are the _LEGACY_HINTS dict; new verbs must not collide with those keys or they will be treated as removed 2.x commands.
Contributor handoff
Edit inside browser_use, keep examples and docs aligned only when behavior changes, and do not add sample scripts. The code style is tabs, single quotes, line length 130, Python 3.11+ unions, and Pydantic v2 models for action schemas. Before changing the agent loop, read Agent.step, BrowserSession.on_BrowserStartEvent, Tools.act, and Registry.execute_action. Run the focused tests/ci file plus pyright; the full matrix is ./bin/test.sh. Known traps are stale defaults in AGENTS.md, the external browser_harness CLI, the commented-out CrashWatchdog, and secret handling that is only domain-scoped when the caller uses the nested dict form.
- CLAUDE.md is the engineering contract: async Python, tabs not spaces, str | None rather than Optional, service.py for logic and views.py for Pydantic models, uuid7str for new ids, and runtime assertions at function boundaries. Logging helpers should be named _log_*.
- Safe first reads for any agent-loop change are browser_use/agent/service.py (Agent.__init__, step, run, multi_act), browser_use/browser/session.py (on_BrowserStartEvent, attach_all_watchdogs), browser_use/tools/service.py, and browser_use/tools/registry/service.py. BrowserProfile fields live in browser_use/browser/profile.py.
- Do not add new example files to show off a feature. AGENTS.md and CLAUDE.md both say to test inline in a terminal instead. Examples that already exist under examples/ should be updated when the API they demonstrate changes.
- Use uv, not pip. Local setup is uv venv --python 3.11, then uv sync. The CI-equivalent test command is ./bin/test.sh or uv run pytest -vxs tests/ci. A single file is uv run pytest -vxs tests/ci/<file>.py. Typecheck with uv run pyright. Format with uv run ruff format and uv run ruff check --fix. Pre-commit is uv run pre-commit run --all-files.
- Action signature trap: parameters injected by the registry must use the exact names in _get_special_param_types, and the function must not declare **kwargs. Custom tools that want the browser must annotate browser_session: BrowserSession. Using browser: Browser silently fails to inject.
- Indentation and formatter trap: ruff format uses indent-style tab and quote-style single, line length 130. A space-indented edit will fail pre-commit. E501 is ignored, so long lines are allowed.
- Default-value trap: trust Agent.__init__ and Agent.run, not the parameter tables copied into AGENTS.md. In this tree run(max_steps=500), max_actions_per_step=5, max_failures=5, step_timeout=180, use_vision=True, and a browser-use provider forces flash_mode.
- CLI trap: browser_use/cli.py is a compatibility shim. New browser commands belong in the browser_harness package, which this repo calls but does not vendor. Adding a subcommand whose name is in _LEGACY_HINTS will be swallowed as a migration error.
- Test trap: do not mock the browser, do not request https://example.com or other public hosts, and do not put new CI tests outside tests/ci or they will not be in the workflow matrix. LLM behavior is scripted with create_mock_llm in tests/ci/conftest.py.
- Secret and telemetry trap: do not log raw sensitive_data values; prompts are redacted but execution substitutes the real strings. Do not commit changes that flip ANONYMIZED_TELEMETRY or that print BROWSER_USE_API_KEY. The PostHog key in telemetry/service.py is a public project key, not a user secret, but device_id is persisted on disk.
- pyright will not catch mistakes in browser_use/beta/service.py, browser_use/llm/oci_raw, or the excluded llm tests. A green typecheck does not cover those files. Beta agent work needs tests/ci/test_beta_agent.py even though the implementation is excluded.
- Event-bus trap: BrowserSession handlers and watchdogs both listen on the same bus. Navigation security is enforced by raising from SecurityWatchdog.on_NavigateToUrlEvent, which depends on handler order. Adding a new navigation path that talks to CDP directly will bypass that check.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 404 | 99% |
| Markdown | 60 | docs/config |
| YAML | 17 | docs/config |
| Shell | 4 | 1% |
| HTML | 3 | docs/config |
| JSON | 1 | docs/config |
| TOML | 1 | 0% |
Start reading here
Probable source entrypoint: pyproject.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
browser_use/ (186 files) skills/ (28 files) static/ (3 files)
Source layout — browser_use/
| Path | Files |
|---|---|
browser_use/beta/ | 2 |
browser_use/llm/ | 39 |
browser_use/tools/ | 8 |
Repository structure
| Top-level path | Files |
|---|---|
browser_use/ | 186 |
tests/ | 129 |
examples/ | 119 |
skills/ | 28 |
.github/ | 18 |
docker/ | 5 |
static/ | 3 |
bin/ | 3 |
Docs found: AGENTS.mdCLAUDE.mdREADME.md
Package
pyproject.toml — browser-use v0.13.10, 31 declared dependencies.
clicorebrowser-use-core==0.13.3;browser-use-core==0.13.3;browser-use-core==0.13.3;browser-use-core==0.13.3;browser-use-core==0.13.3;]awsocivideoexamples
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructionsCLAUDE.md— Claude instructions.env.example— Required env vars.github/workflows/stale-bot.yml— GitHub Actions workflow.github/workflows/publish.yml— GitHub Actions workflow.github/workflows/lint.yml— GitHub Actions workflow.github/workflows/install-script.yml— GitHub Actions workflow.github/workflows/claude.yml— GitHub Actions workflow.github/workflows/eval-on-pr.yml— GitHub Actions workflow
Structural health signals
Average score 9.1/10 across 96 analyzed source files; 2 hotspots surfaced.
browser_use/mcp/server.py— 1294 lines, score 5.7 (Large file, Deep nesting)browser_use/browser/watchdogs/downloads_watchdog.py— 1300 lines, score 6.4 (Large file, Deep nesting)browser_use/browser/demo_mode.py— 922 lines, score 6.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
d8110c5ff87c2026-09-15 — docs: add PZERO OpenAI-compatible provider example (#5579) (#5648)4b328e99c66e2026-09-15 — docs: require the PZERO key explicitly49f73c66dfcb2026-09-15 — Merge remote-tracking branch 'origin/main' into pr5648843819cb81312026-09-13 — docs: point bu-30b-a3b-preview at self-hosting instead of Cloud (#5774)09dbfcbca9192026-09-13 — Merge branch 'main' into agency/agi-729-oss-model-self-host