diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6aa65ad..fa40382 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `agent --json` and `engineer --json` report `status: error` (`engine_failure`) when the run ends without writing its client, instead of `ok` with a `script_path` that does not exist.
- Playwright MCP prompt: `browser_run_code` is now `browser_run_code_unsafe`, and the nonexistent `browser_scroll` tool is no longer listed.
- The Claude SDK message buffer is raised to 32 MB, so a large screenshot no longer aborts a run.
+- Manual capture in Chrome no longer says it uses a "profile copy". It has always started Chrome with a fresh, empty profile, so the message and docs now say to sign in during the capture.
### Security
- Raise security minimums to aiohttp >=3.14.4, certifi >=2026.10.19, multidict >=6.9.1, OpenTelemetry API >=1.45.1, PyJWT >=2.15.0 and urllib3 >=2.8.0, and lock the patched versions. certifi drops the emSign root CAs whose intermediate keys were compromised.
diff --git a/src/reverse_api/browser.py b/src/reverse_api/browser.py
index 1249c06..657a4e6 100644
--- a/src/reverse_api/browser.py
+++ b/src/reverse_api/browser.py
@@ -184,7 +184,7 @@ class ManualBrowser:
"""Manages a Playwright browser session with HAR recording.
Supports two modes:
- - Real Chrome: Uses your actual Chrome browser with existing profile (best for stealth)
+ - Real Chrome: Your installed Chrome with a fresh, empty profile (best for stealth; starts signed out)
- Stealth Chromium: Falls back to Playwright's Chromium with stealth patches
"""
@@ -264,7 +264,11 @@ def _require_playwright(self) -> Playwright:
return self._playwright
def _start_with_real_chrome(self, start_url: str | None = None) -> Path:
- """Start using the real Chrome browser with user's profile."""
+ """Start the installed Chrome with a fresh temporary profile.
+
+ The user's Chrome profile only shows that Chrome is installed; it is not copied,
+ so the capture starts without the user's logins and cookies.
+ """
import shutil
import tempfile
@@ -276,7 +280,7 @@ def _start_with_real_chrome(self, start_url: str | None = None) -> Path:
# Create a temporary profile directory
temp_profile_dir = Path(tempfile.mkdtemp(prefix="chrome_profile_"))
- console.print(" [dim]using real chrome (profile copy)[/dim]")
+ console.print(" [dim]using chrome with a fresh profile · sign in during the capture if the site needs it[/dim]")
console.print(" [yellow]⚠️ please browse in the FIRST tab only[/yellow]")
console.print(" [yellow] (new tabs may not be recorded)[/yellow]")
console.print()
diff --git a/tests/test_manual_browser_profile.py b/tests/test_manual_browser_profile.py
new file mode 100644
index 0000000..29f4437
--- /dev/null
+++ b/tests/test_manual_browser_profile.py
@@ -0,0 +1,36 @@
+"""Manual capture in real Chrome uses a fresh profile, not the user's own.
+
+The user's Chrome profile directory is only checked to see that Chrome is installed.
+Nothing is copied from it, so the capture starts signed out and the message must say so.
+"""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+pytest.importorskip("playwright") # ships in the [manual] extra only
+
+from reverse_api.browser import ManualBrowser # noqa: E402
+
+
+def test_real_chrome_launches_with_a_fresh_profile_and_says_so(tmp_path):
+ user_profile = tmp_path / "Chrome"
+ user_profile.mkdir()
+ browser = ManualBrowser(run_id="testrun", prompt="grab jobs", output_dir=str(tmp_path / "runs"))
+ browser._playwright = MagicMock()
+ browser._playwright.chromium.launch_persistent_context.return_value.pages = []
+
+ with (
+ patch("reverse_api.browser.get_chrome_profile_dir", return_value=user_profile),
+ patch("reverse_api.browser.console") as console,
+ patch.object(browser, "close", return_value=browser.har_path),
+ ):
+ browser._start_with_real_chrome()
+
+ kwargs = browser._playwright.chromium.launch_persistent_context.call_args.kwargs
+ assert kwargs["channel"] == "chrome"
+ assert kwargs["user_data_dir"] != str(user_profile)
+ assert not any(user_profile.iterdir())
+ printed = " ".join(str(call.args[0]) for call in console.print.call_args_list if call.args)
+ assert "fresh profile" in printed
+ assert "profile copy" not in printed
diff --git a/website/content/docs/modes/manual.mdx b/website/content/docs/modes/manual.mdx
index d1d6db8..6a42ba9 100644
--- a/website/content/docs/modes/manual.mdx
+++ b/website/content/docs/modes/manual.mdx
@@ -42,6 +42,8 @@ playwright install chromium
Browse and interact with the website: click, search, scroll, paginate.
+ The browser starts with a fresh profile, without your existing logins, so
+ sign in as part of the capture if the site needs it.
Close the browser when done.