From 610c9614a9078031546e2907fbb793e13924d0bd Mon Sep 17 00:00:00 2001 From: Aneesh Sharma Date: Sat, 15 Aug 2026 17:47:27 +0530 Subject: [PATCH 1/7] fix(browser): honor BROWSER_USE_HEADLESS in BrowserProfile (#5420) - Read BROWSER_USE_HEADLESS env var via default_factory in BrowserProfile - Preserve fallback to display detection when env var is unset - Add unit tests covering env var parsing and overrides Fixes #5420 --- browser_use/browser/profile.py | 13 +++- tests/ci/test_extension_config.py | 113 ++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/browser_use/browser/profile.py b/browser_use/browser/profile.py index 4b5e4a9bb..1e0e82fb3 100644 --- a/browser_use/browser/profile.py +++ b/browser_use/browser/profile.py @@ -25,6 +25,14 @@ def _get_enable_default_extensions_default() -> bool: return True +def _get_headless_default() -> bool | None: + """Get the default value for headless from BROWSER_USE_HEADLESS env var, or None to fall back to display detection.""" + env_val = os.getenv('BROWSER_USE_HEADLESS') + if env_val is not None: + return env_val.lower() not in ('0', 'false', 'no', 'off', '') + return None + + CHROME_DEBUG_PORT = 9242 # use a non-default port to avoid conflicts with other tools / devs using 9222 DOMAIN_OPTIMIZATION_THRESHOLD = 100 # Convert domain lists to sets for O(1) lookup when >= this size CHROME_PROFILE_TRANSIENT_FILE_PATTERNS = ( @@ -419,7 +427,10 @@ class BrowserLaunchArgs(BaseModel): validation_alias=AliasChoices('browser_binary_path', 'chrome_binary_path'), description='Path to the chromium-based browser executable to use.', ) - headless: bool | None = Field(default=None, description='Whether to run the browser in headless or windowed mode.') + headless: bool | None = Field( + default_factory=_get_headless_default, + description='Whether to run the browser in headless or windowed mode. Can be set via BROWSER_USE_HEADLESS environment variable.', + ) args: list[CliArgStr] = Field( default_factory=list, description='List of *extra* CLI args to pass to the browser when launching.' ) diff --git a/tests/ci/test_extension_config.py b/tests/ci/test_extension_config.py index 0493019e6..4eae8c724 100644 --- a/tests/ci/test_extension_config.py +++ b/tests/ci/test_extension_config.py @@ -121,3 +121,116 @@ class TestDisableExtensionsEnvVar: os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original else: os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None) + + +class TestHeadlessEnvVar: + """Test BROWSER_USE_HEADLESS environment variable.""" + + def test_default_value_is_none(self): + """Without env var set, headless default should be None.""" + original = os.environ.pop('BROWSER_USE_HEADLESS', None) + try: + from browser_use.browser.profile import _get_headless_default + + assert _get_headless_default() is None + finally: + if original is not None: + os.environ['BROWSER_USE_HEADLESS'] = original + + @pytest.mark.parametrize( + 'env_value,expected_headless', + [ + # Truthy values for HEADLESS = headless enabled (True) + ('true', True), + ('True', True), + ('TRUE', True), + ('1', True), + ('yes', True), + ('on', True), + # Falsy values for HEADLESS = headless disabled (False) + ('false', False), + ('False', False), + ('FALSE', False), + ('0', False), + ('no', False), + ('off', False), + ('', False), + ], + ) + def test_env_var_values(self, env_value: str, expected_headless: bool): + """Test various env var values are parsed correctly.""" + original = os.environ.get('BROWSER_USE_HEADLESS') + try: + os.environ['BROWSER_USE_HEADLESS'] = env_value + from browser_use.browser.profile import _get_headless_default + + result = _get_headless_default() + assert result is expected_headless, ( + f"Expected headless={expected_headless} for BROWSER_USE_HEADLESS='{env_value}', got {result}" + ) + finally: + if original is not None: + os.environ['BROWSER_USE_HEADLESS'] = original + else: + os.environ.pop('BROWSER_USE_HEADLESS', None) + + def test_browser_profile_uses_env_var(self): + """Test that BrowserProfile picks up the BROWSER_USE_HEADLESS env var.""" + original = os.environ.get('BROWSER_USE_HEADLESS') + try: + # Test with env var set to true + os.environ['BROWSER_USE_HEADLESS'] = 'true' + + from browser_use.browser.profile import BrowserProfile + + profile = BrowserProfile() + assert profile.headless is True, 'BrowserProfile should have headless=True when BROWSER_USE_HEADLESS=true' + + # Test with env var set to false + os.environ['BROWSER_USE_HEADLESS'] = 'false' + profile2 = BrowserProfile() + assert profile2.headless is False, 'BrowserProfile should have headless=False when BROWSER_USE_HEADLESS=false' + finally: + if original is not None: + os.environ['BROWSER_USE_HEADLESS'] = original + else: + os.environ.pop('BROWSER_USE_HEADLESS', None) + + def test_explicit_param_overrides_env_var(self): + """Test that explicit headless parameter overrides env var.""" + original = os.environ.get('BROWSER_USE_HEADLESS') + try: + os.environ['BROWSER_USE_HEADLESS'] = 'true' + + from browser_use.browser.profile import BrowserProfile + + # Explicitly set to False should override env var + profile = BrowserProfile(headless=False) + assert profile.headless is False, 'Explicit param should override env var' + + os.environ['BROWSER_USE_HEADLESS'] = 'false' + profile2 = BrowserProfile(headless=True) + assert profile2.headless is True, 'Explicit param should override env var' + finally: + if original is not None: + os.environ['BROWSER_USE_HEADLESS'] = original + else: + os.environ.pop('BROWSER_USE_HEADLESS', None) + + def test_browser_session_uses_env_var(self): + """Test that BrowserSession picks up the env var via BrowserProfile.""" + original = os.environ.get('BROWSER_USE_HEADLESS') + try: + os.environ['BROWSER_USE_HEADLESS'] = 'true' + + from browser_use.browser import BrowserSession + + session = BrowserSession() + assert session.browser_profile.headless is True, ( + 'BrowserSession should have headless=True when BROWSER_USE_HEADLESS=true' + ) + finally: + if original is not None: + os.environ['BROWSER_USE_HEADLESS'] = original + else: + os.environ.pop('BROWSER_USE_HEADLESS', None) From 1e9ee3e64b5701b653d6a08ac4c5480fdccdb1a7 Mon Sep 17 00:00:00 2001 From: Aneesh Sharma Date: Sat, 15 Aug 2026 17:58:44 +0530 Subject: [PATCH 2/7] test: deduplicate browser config env var tests using pytest parametrization --- tests/ci/test_extension_config.py | 316 ++++++++++-------------------- 1 file changed, 99 insertions(+), 217 deletions(-) diff --git a/tests/ci/test_extension_config.py b/tests/ci/test_extension_config.py index 4eae8c724..2f8e17631 100644 --- a/tests/ci/test_extension_config.py +++ b/tests/ci/test_extension_config.py @@ -1,236 +1,118 @@ -"""Tests for extension configuration environment variables.""" - -import os +"""Tests for browser configuration environment variables.""" import pytest +from browser_use.browser import BrowserSession +from browser_use.browser.profile import ( + BrowserProfile, + _get_enable_default_extensions_default, + _get_headless_default, +) -class TestDisableExtensionsEnvVar: - """Test BROWSER_USE_DISABLE_EXTENSIONS environment variable.""" +TRUTHY_STRINGS = ['true', 'True', 'TRUE', '1', 'yes', 'on'] +FALSY_STRINGS = ['false', 'False', 'FALSE', '0', 'no', 'off', ''] - def test_default_value_is_true(self): - """Without env var set, enable_default_extensions should default to True.""" - # Clear the env var if it exists - original = os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None) - try: - # Import fresh to get the default - from browser_use.browser.profile import _get_enable_default_extensions_default - assert _get_enable_default_extensions_default() is True - finally: - if original is not None: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original +class TestConfigEnvVars: + """Tests for browser profile env var configuration.""" + + def test_default_values_without_env(self, monkeypatch: pytest.MonkeyPatch): + """Verify default values when environment variables are unset.""" + monkeypatch.delenv('BROWSER_USE_DISABLE_EXTENSIONS', raising=False) + monkeypatch.delenv('BROWSER_USE_HEADLESS', raising=False) + + assert _get_enable_default_extensions_default() is True + assert _get_headless_default() is None @pytest.mark.parametrize( - 'env_value,expected_enabled', + 'env_var,getter,truthy_expected,falsy_expected', [ - # Truthy values for DISABLE = extensions disabled (False) - ('true', False), - ('True', False), - ('TRUE', False), - ('1', False), - ('yes', False), - ('on', False), - # Falsy values for DISABLE = extensions enabled (True) - ('false', True), - ('False', True), - ('FALSE', True), - ('0', True), - ('no', True), - ('off', True), - ('', True), + ('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, False, True), + ('BROWSER_USE_HEADLESS', _get_headless_default, True, False), ], ) - def test_env_var_values(self, env_value: str, expected_enabled: bool): - """Test various env var values are parsed correctly.""" - original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS') - try: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = env_value - from browser_use.browser.profile import _get_enable_default_extensions_default - - result = _get_enable_default_extensions_default() - assert result is expected_enabled, ( - f"Expected enable_default_extensions={expected_enabled} for DISABLE_EXTENSIONS='{env_value}', got {result}" - ) - finally: - if original is not None: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original - else: - os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None) - - def test_browser_profile_uses_env_var(self): - """Test that BrowserProfile picks up the env var.""" - original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS') - try: - # Test with env var set to true (disable extensions) - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = 'true' - - from browser_use.browser.profile import BrowserProfile - - profile = BrowserProfile(headless=True) - assert profile.enable_default_extensions is False, ( - 'BrowserProfile should disable extensions when BROWSER_USE_DISABLE_EXTENSIONS=true' - ) - - # Test with env var set to false (enable extensions) - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = 'false' - profile2 = BrowserProfile(headless=True) - assert profile2.enable_default_extensions is True, ( - 'BrowserProfile should enable extensions when BROWSER_USE_DISABLE_EXTENSIONS=false' - ) - - finally: - if original is not None: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original - else: - os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None) - - def test_explicit_param_overrides_env_var(self): - """Test that explicit enable_default_extensions parameter overrides env var.""" - original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS') - try: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = 'true' - - from browser_use.browser.profile import BrowserProfile - - # Explicitly set to True should override env var - profile = BrowserProfile(headless=True, enable_default_extensions=True) - assert profile.enable_default_extensions is True, 'Explicit param should override env var' - - finally: - if original is not None: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original - else: - os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None) - - def test_browser_session_uses_env_var(self): - """Test that BrowserSession picks up the env var via BrowserProfile.""" - original = os.environ.get('BROWSER_USE_DISABLE_EXTENSIONS') - try: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = '1' - - from browser_use.browser import BrowserSession - - session = BrowserSession(headless=True) - assert session.browser_profile.enable_default_extensions is False, ( - 'BrowserSession should disable extensions when BROWSER_USE_DISABLE_EXTENSIONS=1' - ) - - finally: - if original is not None: - os.environ['BROWSER_USE_DISABLE_EXTENSIONS'] = original - else: - os.environ.pop('BROWSER_USE_DISABLE_EXTENSIONS', None) - - -class TestHeadlessEnvVar: - """Test BROWSER_USE_HEADLESS environment variable.""" - - def test_default_value_is_none(self): - """Without env var set, headless default should be None.""" - original = os.environ.pop('BROWSER_USE_HEADLESS', None) - try: - from browser_use.browser.profile import _get_headless_default - - assert _get_headless_default() is None - finally: - if original is not None: - os.environ['BROWSER_USE_HEADLESS'] = original + def test_env_var_truthy_values( + self, + monkeypatch: pytest.MonkeyPatch, + env_var: str, + getter, + truthy_expected: bool, + falsy_expected: bool, + ): + """Test truthy env var values are parsed correctly.""" + for val in TRUTHY_STRINGS: + monkeypatch.setenv(env_var, val) + assert getter() is truthy_expected, f'Failed for {env_var}={val}' @pytest.mark.parametrize( - 'env_value,expected_headless', + 'env_var,getter,truthy_expected,falsy_expected', [ - # Truthy values for HEADLESS = headless enabled (True) - ('true', True), - ('True', True), - ('TRUE', True), - ('1', True), - ('yes', True), - ('on', True), - # Falsy values for HEADLESS = headless disabled (False) - ('false', False), - ('False', False), - ('FALSE', False), - ('0', False), - ('no', False), - ('off', False), - ('', False), + ('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, False, True), + ('BROWSER_USE_HEADLESS', _get_headless_default, True, False), ], ) - def test_env_var_values(self, env_value: str, expected_headless: bool): - """Test various env var values are parsed correctly.""" - original = os.environ.get('BROWSER_USE_HEADLESS') - try: - os.environ['BROWSER_USE_HEADLESS'] = env_value - from browser_use.browser.profile import _get_headless_default + def test_env_var_falsy_values( + self, + monkeypatch: pytest.MonkeyPatch, + env_var: str, + getter, + truthy_expected: bool, + falsy_expected: bool, + ): + """Test falsy env var values are parsed correctly.""" + for val in FALSY_STRINGS: + monkeypatch.setenv(env_var, val) + assert getter() is falsy_expected, f'Failed for {env_var}={val}' - result = _get_headless_default() - assert result is expected_headless, ( - f"Expected headless={expected_headless} for BROWSER_USE_HEADLESS='{env_value}', got {result}" - ) - finally: - if original is not None: - os.environ['BROWSER_USE_HEADLESS'] = original - else: - os.environ.pop('BROWSER_USE_HEADLESS', None) + @pytest.mark.parametrize( + 'env_var,attr_name,truthy_val,falsy_val', + [ + ('BROWSER_USE_DISABLE_EXTENSIONS', 'enable_default_extensions', False, True), + ('BROWSER_USE_HEADLESS', 'headless', True, False), + ], + ) + def test_browser_profile_and_session_env_var( + self, + monkeypatch: pytest.MonkeyPatch, + env_var: str, + attr_name: str, + truthy_val: bool, + falsy_val: bool, + ): + """Test that BrowserProfile and BrowserSession pick up env vars.""" + # Test truthy env value + monkeypatch.setenv(env_var, 'true') + profile = BrowserProfile() + assert getattr(profile, attr_name) is truthy_val + session = BrowserSession() + assert getattr(session.browser_profile, attr_name) is truthy_val - def test_browser_profile_uses_env_var(self): - """Test that BrowserProfile picks up the BROWSER_USE_HEADLESS env var.""" - original = os.environ.get('BROWSER_USE_HEADLESS') - try: - # Test with env var set to true - os.environ['BROWSER_USE_HEADLESS'] = 'true' + # Test falsy env value + monkeypatch.setenv(env_var, 'false') + profile_falsy = BrowserProfile() + assert getattr(profile_falsy, attr_name) is falsy_val + session_falsy = BrowserSession() + assert getattr(session_falsy.browser_profile, attr_name) is falsy_val - from browser_use.browser.profile import BrowserProfile - - profile = BrowserProfile() - assert profile.headless is True, 'BrowserProfile should have headless=True when BROWSER_USE_HEADLESS=true' - - # Test with env var set to false - os.environ['BROWSER_USE_HEADLESS'] = 'false' - profile2 = BrowserProfile() - assert profile2.headless is False, 'BrowserProfile should have headless=False when BROWSER_USE_HEADLESS=false' - finally: - if original is not None: - os.environ['BROWSER_USE_HEADLESS'] = original - else: - os.environ.pop('BROWSER_USE_HEADLESS', None) - - def test_explicit_param_overrides_env_var(self): - """Test that explicit headless parameter overrides env var.""" - original = os.environ.get('BROWSER_USE_HEADLESS') - try: - os.environ['BROWSER_USE_HEADLESS'] = 'true' - - from browser_use.browser.profile import BrowserProfile - - # Explicitly set to False should override env var - profile = BrowserProfile(headless=False) - assert profile.headless is False, 'Explicit param should override env var' - - os.environ['BROWSER_USE_HEADLESS'] = 'false' - profile2 = BrowserProfile(headless=True) - assert profile2.headless is True, 'Explicit param should override env var' - finally: - if original is not None: - os.environ['BROWSER_USE_HEADLESS'] = original - else: - os.environ.pop('BROWSER_USE_HEADLESS', None) - - def test_browser_session_uses_env_var(self): - """Test that BrowserSession picks up the env var via BrowserProfile.""" - original = os.environ.get('BROWSER_USE_HEADLESS') - try: - os.environ['BROWSER_USE_HEADLESS'] = 'true' - - from browser_use.browser import BrowserSession - - session = BrowserSession() - assert session.browser_profile.headless is True, ( - 'BrowserSession should have headless=True when BROWSER_USE_HEADLESS=true' - ) - finally: - if original is not None: - os.environ['BROWSER_USE_HEADLESS'] = original - else: - os.environ.pop('BROWSER_USE_HEADLESS', None) + @pytest.mark.parametrize( + 'env_var,attr_name,env_val,explicit_arg,expected', + [ + ('BROWSER_USE_DISABLE_EXTENSIONS', 'enable_default_extensions', 'true', {'enable_default_extensions': True}, True), + ('BROWSER_USE_DISABLE_EXTENSIONS', 'enable_default_extensions', 'false', {'enable_default_extensions': False}, False), + ('BROWSER_USE_HEADLESS', 'headless', 'true', {'headless': False}, False), + ('BROWSER_USE_HEADLESS', 'headless', 'false', {'headless': True}, True), + ], + ) + def test_explicit_parameter_overrides_env_var( + self, + monkeypatch: pytest.MonkeyPatch, + env_var: str, + attr_name: str, + env_val: str, + explicit_arg: dict, + expected: bool, + ): + """Test that explicit constructor parameters override env vars.""" + monkeypatch.setenv(env_var, env_val) + profile = BrowserProfile(**explicit_arg) + assert getattr(profile, attr_name) is expected From 7c6ac585f1438c0c9825f6426421a04f150183df Mon Sep 17 00:00:00 2001 From: Aneesh Sharma Date: Sat, 15 Aug 2026 18:56:52 +0530 Subject: [PATCH 3/7] test: remove unused parameters in env var truthy/falsy test signatures --- tests/ci/test_extension_config.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/ci/test_extension_config.py b/tests/ci/test_extension_config.py index 2f8e17631..6bb1e7d96 100644 --- a/tests/ci/test_extension_config.py +++ b/tests/ci/test_extension_config.py @@ -25,10 +25,10 @@ class TestConfigEnvVars: assert _get_headless_default() is None @pytest.mark.parametrize( - 'env_var,getter,truthy_expected,falsy_expected', + 'env_var,getter,expected', [ - ('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, False, True), - ('BROWSER_USE_HEADLESS', _get_headless_default, True, False), + ('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, False), + ('BROWSER_USE_HEADLESS', _get_headless_default, True), ], ) def test_env_var_truthy_values( @@ -36,19 +36,18 @@ class TestConfigEnvVars: monkeypatch: pytest.MonkeyPatch, env_var: str, getter, - truthy_expected: bool, - falsy_expected: bool, + expected: bool, ): """Test truthy env var values are parsed correctly.""" for val in TRUTHY_STRINGS: monkeypatch.setenv(env_var, val) - assert getter() is truthy_expected, f'Failed for {env_var}={val}' + assert getter() is expected, f'Failed for {env_var}={val}' @pytest.mark.parametrize( - 'env_var,getter,truthy_expected,falsy_expected', + 'env_var,getter,expected', [ - ('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, False, True), - ('BROWSER_USE_HEADLESS', _get_headless_default, True, False), + ('BROWSER_USE_DISABLE_EXTENSIONS', _get_enable_default_extensions_default, True), + ('BROWSER_USE_HEADLESS', _get_headless_default, False), ], ) def test_env_var_falsy_values( @@ -56,13 +55,12 @@ class TestConfigEnvVars: monkeypatch: pytest.MonkeyPatch, env_var: str, getter, - truthy_expected: bool, - falsy_expected: bool, + expected: bool, ): """Test falsy env var values are parsed correctly.""" for val in FALSY_STRINGS: monkeypatch.setenv(env_var, val) - assert getter() is falsy_expected, f'Failed for {env_var}={val}' + assert getter() is expected, f'Failed for {env_var}={val}' @pytest.mark.parametrize( 'env_var,attr_name,truthy_val,falsy_val', From 5525c08c54bbdcb7cfacde1474ba8c566297012c Mon Sep 17 00:00:00 2001 From: MagMueller Date: Thu, 27 Aug 2026 23:23:42 -0700 Subject: [PATCH 4/7] docs: migrate remote browser skill to CLI 3.0 --- skills/remote-browser/SKILL.md | 198 +++++------------- .../ci/test_browser_use_skill_install_docs.py | 26 +++ 2 files changed, 81 insertions(+), 143 deletions(-) diff --git a/skills/remote-browser/SKILL.md b/skills/remote-browser/SKILL.md index 9c8b0efe9..7c0e52d6b 100644 --- a/skills/remote-browser/SKILL.md +++ b/skills/remote-browser/SKILL.md @@ -1,179 +1,91 @@ --- name: remote-browser -description: Controls a local browser from a sandboxed remote machine. Use when the agent is running in a sandbox (no GUI) and needs to navigate websites, interact with web pages, fill forms, take screenshots, or expose local dev servers via tunnels. +description: Controls an isolated Browser Use Cloud browser from a sandboxed machine with the current Browser Use CLI. allowed-tools: Bash(browser-use:*) --- -# Browser Automation for Sandboxed Agents +# Remote Browser -This skill is for agents running on **sandboxed remote machines** (cloud VMs, CI, coding agents) that need to control a headless browser. +Use this skill when an agent runs on a machine without a usable local Chrome and needs an isolated browser. The current Browser Use CLI runs Python from stdin. Do not use the removed `open`, `state`, `click`, `input`, `tab`, `cloud connect`, or `--connect` commands. -## Prerequisites +## Check the CLI ```bash -browser-use doctor # Verify installation +browser-use --doctor +browser-use skill show ``` -For setup details, see https://github.com/browser-use/browser-use/blob/main/browser_use/skill_cli/README.md +If setup fails, follow the current [Browser Use skill](../browser-use/SKILL.md). -## Core Workflow +## Start an isolated browser -1. **Navigate**: `browser-use open ` — starts headless browser if needed -2. **Inspect**: `browser-use state` — returns clickable elements with indices -3. **Interact**: use indices from state (`browser-use click 5`, `browser-use input 3 "text"`) -4. **Verify**: `browser-use state` or `browser-use screenshot` to confirm -5. **Repeat**: browser stays open between commands -6. **Cleanup**: `browser-use close` when done - -## Browser Modes +Authenticate once: ```bash -browser-use open # Default: headless Chromium -browser-use cloud connect # Provision cloud browser and connect -browser-use --connect open # Auto-discover running Chrome via CDP -browser-use --cdp-url ws://localhost:9222/... open # Connect via CDP URL +browser-use auth login ``` -## Commands +Pick a short unique name. `r7k2` below is only an example. ```bash -# Navigation -browser-use open # Navigate to URL -browser-use back # Go back in history -browser-use scroll down # Scroll down (--amount N for pixels) -browser-use scroll up # Scroll up -browser-use tab list # List all tabs with lock status -browser-use tab new [url] # Open a new tab (blank or with URL) -browser-use tab switch # Switch to tab by index -browser-use tab close [index...] # Close one or more tabs - -# Page State — always run state first to get element indices -browser-use state # URL, title, clickable elements with indices -browser-use screenshot [path.png] # Screenshot (base64 if no path, --full for full page) - -# Interactions — use indices from state -browser-use click # Click element by index -browser-use click # Click at pixel coordinates -browser-use type "text" # Type into focused element -browser-use input "text" # Click element, then type -browser-use keys "Enter" # Send keyboard keys (also "Control+a", etc.) -browser-use select "option" # Select dropdown option -browser-use upload # Upload file to file input -browser-use hover # Hover over element -browser-use dblclick # Double-click element -browser-use rightclick # Right-click element - -# Data Extraction -browser-use eval "js code" # Execute JavaScript, return result -browser-use get title # Page title -browser-use get html [--selector "h1"] # Page HTML (or scoped to selector) -browser-use get text # Element text content -browser-use get value # Input/textarea value -browser-use get attributes # Element attributes -browser-use get bbox # Bounding box (x, y, width, height) - -# Wait -browser-use wait selector "css" # Wait for element (--state visible|hidden|attached|detached, --timeout ms) -browser-use wait text "text" # Wait for text to appear - -# Cookies -browser-use cookies get [--url ] # Get cookies (optionally filtered) -browser-use cookies set # Set cookie (--domain, --secure, --http-only, --same-site, --expires) -browser-use cookies clear [--url ] # Clear cookies -browser-use cookies export # Export to JSON -browser-use cookies import # Import from JSON - -# Python — persistent session with browser access -browser-use python "code" # Execute Python (variables persist across calls) -browser-use python --file script.py # Run file -browser-use python --vars # Show defined variables -browser-use python --reset # Clear namespace - -# Session -browser-use close # Close browser and stop daemon -browser-use sessions # List active sessions -browser-use close --all # Close all sessions +browser-use <<'PY' +start_remote_daemon("r7k2") +PY ``` -The Python `browser` object provides: `browser.url`, `browser.title`, `browser.html`, `browser.goto(url)`, `browser.back()`, `browser.click(index)`, `browser.type(text)`, `browser.input(index, text)`, `browser.keys(keys)`, `browser.upload(index, path)`, `browser.screenshot(path)`, `browser.scroll(direction, amount)`, `browser.wait(seconds)`. - -## Tunnels - -Expose local dev servers to the browser via Cloudflare tunnels. +Use the same name for every command in this browser: ```bash -browser-use tunnel # Start tunnel (idempotent) -browser-use tunnel list # Show active tunnels -browser-use tunnel stop # Stop tunnel -browser-use tunnel stop --all # Stop all tunnels +BU_NAME=r7k2 browser-use <<'PY' +new_tab("https://example.com") +wait_for_load() +print(page_info()) +PY ``` -## Command Chaining +Each remote daemon is a separate Browser Use Cloud browser. Use a different name for each parallel task. Remote browsers can bill until they stop or time out. -Commands can be chained with `&&`. The browser persists via the daemon, so chaining is safe and efficient. +## Inspect and interact + +Helpers are pre-imported. Keep multi-step work in one heredoc when practical. ```bash -browser-use open https://example.com && browser-use state -browser-use input 5 "user@example.com" && browser-use input 6 "password" && browser-use click 7 +BU_NAME=r7k2 browser-use <<'PY' +print(page_info()) +print(js("document.title")) + +fill_input('input[name="q"]', "browser automation") +press_key("Enter") +wait_for_load() + +print(page_info()) +PY ``` -Chain when you don't need intermediate output. Run separately when you need to parse `state` to discover indices first. +Useful helpers: -## Common Workflows +- Navigate: `new_tab(url)`, `goto_url(url)`, `wait_for_load()` +- Inspect: `page_info()`, `js(code)`, `cdp(method, ...)` +- Interact: `click_at_xy(x, y)`, `type_text(text)`, `fill_input(selector, text)`, `press_key(key)`, `scroll(x, y)` +- Tabs: `list_tabs()`, `switch_tab(target)`, `close_tab(target)` +- Files and proof: `capture_screenshot()`, `wait_for_element(selector)` -### Exposing Local Dev Servers +Prefer the accessibility tree for element discovery: + +```python +nodes = cdp("Accessibility.getFullAXTree")["nodes"] +``` + +Use a targeted `js(...)` query when the accessibility tree lacks the element. Verify each action with `page_info()`, a focused DOM check, or a screenshot. + +## Stop the browser + +When the work is done, stop the exact named browser: ```bash -python -m http.server 3000 & # Start dev server -browser-use tunnel 3000 # → https://abc.trycloudflare.com -browser-use open https://abc.trycloudflare.com # Browse the tunnel +browser-use <<'PY' +stop_remote_daemon("r7k2") +PY ``` -Tunnels are independent of browser sessions and persist across `browser-use close`. - -## Multi-Agent (--connect mode) - -Multiple agents can share one browser via `--connect`. Each agent gets its own tab — other agents can't interfere. - -**Setup**: Register once, then pass the index with every `--connect` command: - -```bash -INDEX=$(browser-use register) # → prints "1" -browser-use --connect $INDEX open # Navigate in agent's own tab -browser-use --connect $INDEX state # Get state from agent's tab -browser-use --connect $INDEX click # Click in agent's tab -``` - -- **Tab locking**: When an agent mutates a tab (click, type, navigate), that tab is locked to it. Other agents get an error if they try to mutate the same tab. -- **Read-only access**: `state`, `screenshot`, `get`, and `wait` commands work on any tab regardless of locks. -- **Agent sessions expire** after 5 minutes of inactivity. Run `browser-use register` again to get a new index. - -## Global Options - -| Option | Description | -|--------|-------------| -| `--headed` | Show browser window | -| `--connect` | Auto-discover running Chrome via CDP | -| `--cdp-url ` | Connect via CDP URL (`http://` or `ws://`) | -| `--session NAME` | Target a named session (default: "default") | -| `--json` | Output as JSON | - -## Tips - -1. **Always run `state` first** to see available elements and their indices -2. **Sessions persist** — browser stays open between commands until you close it -3. **Tunnels are independent** — they persist across `browser-use close` -4. **`tunnel` is idempotent** — calling again for the same port returns the existing URL - -## Troubleshooting - -- **Browser won't start?** `browser-use close` then retry. Run `browser-use doctor` to check. -- **Element not found?** `browser-use scroll down` then `browser-use state` -- **Tunnel not working?** `which cloudflared` to check, `browser-use tunnel list` to see active tunnels - -## Cleanup - -```bash -browser-use close # Close browser session -browser-use tunnel stop --all # Stop tunnels (if any) -``` +Do not leave an unused remote browser running. diff --git a/tests/ci/test_browser_use_skill_install_docs.py b/tests/ci/test_browser_use_skill_install_docs.py index 41da21a81..878e570a6 100644 --- a/tests/ci/test_browser_use_skill_install_docs.py +++ b/tests/ci/test_browser_use_skill_install_docs.py @@ -57,6 +57,32 @@ def test_docs_install_browser_use_skill_from_package_alias(): assert 'raw.githubusercontent.com/browser-use/browser-harness/main/SKILL.md' not in readme +def test_remote_browser_skill_uses_current_cli(): + remote_skill = (ROOT / 'skills' / 'remote-browser' / 'SKILL.md').read_text(encoding='utf-8') + + for removed_command in ( + 'browser-use open', + 'browser-use state', + 'browser-use click', + 'browser-use input', + 'browser-use tab', + 'browser-use cloud connect', + 'browser-use --connect', + 'browser_use/skill_cli/README.md', + ): + assert removed_command not in remote_skill + + for current_command in ( + "browser-use <<'PY'", + 'start_remote_daemon("r7k2")', + 'BU_NAME=r7k2 browser-use', + 'new_tab("https://example.com")', + 'print(page_info())', + 'stop_remote_daemon("r7k2")', + ): + assert current_command in remote_skill + + def test_browser_use_cli_installs_browser_harness_package_skill(tmp_path): bin_dir = _fake_browser_harness_tools(tmp_path, '---\nname: browser-harness\n---\n\n# Browser Harness\n') From 7ed622f0de3ec8733b40ab7c7a6a94e6f9e5bcba Mon Sep 17 00:00:00 2001 From: MagMueller Date: Thu, 27 Aug 2026 23:28:44 -0700 Subject: [PATCH 5/7] test: cover all retired browser commands --- tests/ci/test_browser_use_skill_install_docs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/ci/test_browser_use_skill_install_docs.py b/tests/ci/test_browser_use_skill_install_docs.py index 878e570a6..933614c1c 100644 --- a/tests/ci/test_browser_use_skill_install_docs.py +++ b/tests/ci/test_browser_use_skill_install_docs.py @@ -66,6 +66,14 @@ def test_remote_browser_skill_uses_current_cli(): 'browser-use click', 'browser-use input', 'browser-use tab', + 'browser-use screenshot', + 'browser-use eval', + 'browser-use cookies', + 'browser-use close', + 'browser-use sessions', + 'browser-use tunnel', + 'browser-use wait', + 'browser-use register', 'browser-use cloud connect', 'browser-use --connect', 'browser_use/skill_cli/README.md', From b4bad86d21bf693b9ae5748b667fc32fc8b8e7d5 Mon Sep 17 00:00:00 2001 From: MagMueller Date: Wed, 26 Aug 2026 09:27:15 -0700 Subject: [PATCH 6/7] fix: allow pydantic 2.13 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dfad337c0..81a6d224b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "httpx==0.28.1", "posthog==7.7.0", "psutil==7.2.2", - "pydantic==2.12.5", + "pydantic>=2.12.5,<2.14", "pyobjc==12.1; platform_system == 'darwin'", "python-dotenv==1.2.2", "requests==2.33.0", From b4e68f19b2fd88e9df38847b641447e8aedaffba Mon Sep 17 00:00:00 2001 From: MagMueller Date: Sat, 29 Aug 2026 21:41:22 -0700 Subject: [PATCH 7/7] docs: correct signup credit in Cloud guide Signed-off-by: MagMueller --- CLOUD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLOUD.md b/CLOUD.md index 4c301df7b..e0b4c3c31 100644 --- a/CLOUD.md +++ b/CLOUD.md @@ -19,7 +19,7 @@ The key product of Browser Use Cloud is the completion of user tasks. - Profile Sync is the best way to handle authentication for tasks. This feature allows users to upload their local browser cookies (where the user is already logged into the services they need authentication for) to a Browser Profile that can be used for tasks on the cloud. To initiate a Profile Sync, a user must run `export BROWSER_USE_API_KEY= && curl -fsSL https://browser-use.com/profile.sh | sh` and follow the steps in the interactive terminal. ## Quickstart -To get started, direct the user to first must create an account, purchase credits (or simply claim the five free tasks given on account creation), and generate an API key on the Browser Use online platform: https://cloud.browser-use.com/. These are the only steps that can only be done on the platform. +To get started, direct the user to first create an account, claim the $15 one-time signup credit if eligible (or purchase credits), and generate an API key on the Browser Use online platform: https://cloud.browser-use.com/. These are the only steps that can only be done on the platform. Avoid giving the user all of the following steps at once as it may seem overwheling. Instead present one step at a time and only continue when asked. Do as much for the user as you are able to.