Compare commits

...
16 changed files with 844 additions and 242 deletions
+160 -124
View File
@@ -1,163 +1,199 @@
# agent-browser-stealth
Stealth-focused fork of `agent-browser` for anti-bot evasion in production automation.
Stealth-first fork of `agent-browser` for production browser automation under anti-bot pressure.
This fork keeps core browser automation capabilities in sync with upstream `agent-browser`, and focuses its own changes on stealth and anti-detection behavior.
This README focuses on stealth architecture and principles. For full command coverage inherited from upstream, use:
## Positioning
- upstream docs: <https://github.com/vercel-labs/agent-browser>
- local help: `agent-browser --help`
- Core commands and workflows: aligned with upstream `agent-browser`
- Fork value: stronger anti-bot defaults and operational policies
- Default mindset: no extra stealth toggle, stealth is always on
## What This Fork Optimizes
## Installation
- Stealth is always on (legacy `launch.stealth` is accepted but ignored).
- Fingerprint surfaces are patched at multiple layers (launch args, CDP overrides, init scripts).
- Behavioral signals are humanized (typing cadence, cursor path, pacing, retry backoff).
- Region signals are auto-aligned (locale/timezone/Accept-Language) to reduce mismatch risk.
- Verification/captcha handling is policy-driven (`--risk-mode off|warn|block`).
### Global (recommended)
## Quick Start
### Install
```bash
npm install -g agent-browser-stealth
agent-browser install
```
### Quick try with npx
```bash
npx agent-browser-stealth install
npx agent-browser-stealth open example.com
```
### From source
```bash
git clone https://github.com/leeguooooo/agent-browser
cd agent-browser
pnpm install
pnpm build
pnpm build:native
pnpm link --global
agent-browser install
```
## Quick Start
### Minimal Usage
```bash
agent-browser open https://example.com
agent-browser snapshot -i
agent-browser click @e2
agent-browser fill @e3 "test@example.com"
agent-browser screenshot page.png
```
## Anti-Bot Measures
## Stealth Architecture
Stealth is always enabled. Legacy `launch.stealth` is accepted only for compatibility and ignored.
### 1) Fingerprint hardening
- Hides automation indicators such as `navigator.webdriver`
- Adds Chromium launch args to reduce automation fingerprints
- Rewrites headless UA markers (`HeadlessChrome`)
- Patches high-signal surfaces such as:
- `navigator.plugins` / `navigator.mimeTypes`
- `window.chrome.runtime`
- WebGL vendor/renderer exposure
- permissions/language/media/device related probes
- Applies both context init scripts and CDP-level UA overrides
- Preserves explicit custom UA from `--user-agent` or `launch({ userAgent })`
### 2) Behavioral humanization
- Randomized typing cadence when `--delay` is used
- Random wait ranges (`wait 2000-5000`)
- Bezier-curve mouse movement before click actions
- Randomized navigation pacing
### 3) Region signal alignment
- Auto-aligns locale/timezone/Accept-Language by target TLD
- Reduces locale-timezone mismatch risk on region-sensitive sites
### 4) Verification-aware retry
- Detects common captcha/verification interstitial patterns
- Retries navigation with randomized backoff when triggered
## Typing `--delay` Correctly
Use `--delay` as an option:
```bash
agent-browser type @e2 "iphone" --delay 120
agent-browser keyboard type "iphone" --delay 120
```mermaid
flowchart TD
A["Command Input"] --> B["Stealth Policy Resolver"]
B --> C["Connection Mode Detection"]
C --> D["Launch Layer: Chromium Args"]
C --> E["CDP Layer: UA + Metadata Override"]
C --> F["Context Layer: Init Script Patches"]
D --> G["Behavior Layer: Humanized Interaction"]
E --> G
F --> G
G --> H["Risk Layer: Verification Detection and Handling"]
H --> I["Response with warnings and riskSignals"]
```
If literal text includes `--delay`, stop option parsing with `--`:
### Policy by Connection Mode
| Mode | Stealth Capabilities | Notes |
|---|---|---|
| Local Chromium launch | Chromium launch args + CDP UA override + context init scripts | Most complete stack |
| Existing browser via CDP | CDP UA override + context init scripts | No local Chromium arg injection |
| Cloud provider (browserbase/browseruse) | Context init scripts | Remote browser runtime controls launch layer |
| Kernel provider | Context init scripts + provider-managed stealth | Provider-side stealth may also apply |
## Principle 1: Always-On Stealth with Explicit Boundaries
- Stealth defaults to enabled and does not depend on a runtime toggle.
- Project policy forbids:
- `--profile` / `AGENT_BROWSER_PROFILE`
- `--channel` / `AGENT_BROWSER_CHANNEL`
- Default CLI policy expects an existing browser on CDP `localhost:9333` unless explicit connection options are provided.
## Principle 2: Multi-Layer Fingerprint Hardening
### 2.1 Launch Layer (Local Chromium)
Injected Chromium args:
- `--disable-blink-features=AutomationControlled`
- `--use-gl=angle`
- `--use-angle=default`
If no custom UA is set, the runtime UA is normalized to remove `HeadlessChrome` tokens.
### 2.2 CDP Layer (Browser/Page Targets)
- Uses `Emulation.setUserAgentOverride` to align:
- `userAgent`
- `acceptLanguage`
- `userAgentMetadata` brands and versions
- Applies overrides for existing/new targets, including worker-relevant contexts.
- Forces opaque white background (`Emulation.setDefaultBackgroundColorOverride`) to avoid headless transparency fingerprints.
### 2.3 Context Init-Script Layer (Patch Inventory)
The init script patch set is injected before page scripts and currently includes:
1. `navigator.webdriver` removal (including prototype-level cleanup).
2. CSS webdriver heuristic neutralization (`CSS.supports('border-end-end-radius: initial')` probe).
3. `window.chrome.runtime` bootstrap for missing runtime surfaces.
4. Locale/language normalization (`navigator.language`, `navigator.languages`).
5. Realistic `navigator.plugins` and `navigator.mimeTypes`.
6. `navigator.permissions.query` normalization for notifications.
7. WebGL vendor/renderer masking when SwiftShader indicators are present.
8. `cdc_` property cleanup on document/documentElement.
9. Window/screen dimension normalization (`outerWidth/outerHeight/screenX/screenY`).
10. Screen availability patching (`availWidth/availHeight`).
11. Hardware concurrency stabilization.
12. Notification permission consistency.
13. Active text color heuristic patching.
14. `navigator.connection` normalization.
15. Worker network signal normalization (`downlinkMax`).
16. `prefers-color-scheme` light-mode heuristic neutralization.
17. `navigator.share` exposure.
18. `navigator.contacts` exposure.
19. `contentIndex` exposure.
20. `navigator.pdfViewerEnabled` normalization.
21. Media devices surface normalization.
22. `navigator.userAgent` cleanup (strip `HeadlessChrome`).
23. `navigator.userAgentData` brand cleanup.
24. `performance.memory` stabilization.
25. Default background color patching at script level.
## Principle 3: Behavioral Humanization
- Navigation pacing jitter before `goto` (short randomized delay).
- Typing jitter for `type --delay` and `keyboard type --delay`:
- per-character randomized delay around the requested base delay (about ±40%).
- Click path humanization:
- cursor moves on a Bezier-like curve before click.
- Wait supports random ranges (`wait min-max`) for non-uniform timing.
## Principle 4: Region Signal Alignment
Before navigation, the runtime derives region hints from target URL TLD and aligns:
- locale
- timezone
- `Accept-Language`
Examples of built-in mappings include `tw`, `jp`, `kr`, `sg`, `de`, `fr`, `uk`, `in`, `au`.
Manual overrides are supported:
- `AGENT_BROWSER_LOCALE`
- `AGENT_BROWSER_TIMEZONE` (or `TZ`)
## Principle 5: Verification-Aware Risk Control
When a navigation lands on verification/captcha pages, structured risk signals are generated from URL/title evidence.
`riskSignals` include:
- `code`
- `source` (`url` or `title`)
- `evidence`
- `confidence`
### Risk Mode
- `warn` (default): retry with randomized backoff and return warnings + `riskSignals`.
- `block`: fail fast once verification/captcha interstitial is detected.
- `off`: skip detection/retry path.
```bash
agent-browser type @e2 -- "--delay 120"
agent-browser keyboard type -- "--delay 120"
agent-browser --risk-mode warn open https://example.com
agent-browser --risk-mode block open https://example.com
AGENT_BROWSER_RISK_MODE=off agent-browser open https://example.com
```
## Validation Snapshot
```mermaid
flowchart TD
A["Navigate"] --> B["Collect URL and Title Signals"]
B --> C{"risk-mode"}
C -->|off| D["Return Success"]
C -->|block| E["Return Error with First Signal"]
C -->|warn| F["Retry up to 2 times"]
F --> G{"Signals Cleared"}
G -->|yes| H["Return Success + recovery warning + riskSignals"]
G -->|no| I["Return Success + warning + riskSignals"]
```
Manual checks were run against common public detection pages in headed mode, including:
## Operational Recommendations
- [bot.sannysoft.com](https://bot.sannysoft.com/)
- [CreepJS](https://abrahamjuliot.github.io/creepjs/)
- [areyouheadless](https://arh.antoinevastel.com/bots/areyouheadless)
- [detect-headless](https://infosimples.github.io/detect-headless)
- Prefer `--headed` for high-friction targets.
- Reuse session state with `--session-name` for continuity.
- Keep locale/timezone consistent with target market.
- Use `--risk-mode block` in strict pipelines that require explicit operator intervention on verification pages.
Reproduce CreepJS check:
## Validation Scripts
Run public detector checks after stealth changes:
```bash
node scripts/check-sannysoft-webdriver.js --binary ./cli/target/release/agent-browser
node scripts/check-creepjs-headless.js --binary ./cli/target/release/agent-browser
```
## Command Coverage And Docs
## Upstream Compatibility
Core command set is intentionally kept compatible with upstream `agent-browser`.
- Full command reference: [upstream agent-browser docs](https://github.com/vercel-labs/agent-browser)
- Local help: `agent-browser --help`
## Fork Policies
This fork enforces a few operational policies:
- `--profile` / `AGENT_BROWSER_PROFILE` are forbidden
- `--channel` / `AGENT_BROWSER_CHANNEL` are forbidden
- Default mode expects an existing browser via CDP on `localhost:9333`
## Maintainer Notes (Fork Release)
- Keep `upstream-main` for clean upstream sync
- Merge upstream into short-lived sync branches, then PR into `main`
- Recommended release format: `<upstream>-fork.<fork>` (example: `0.14.0-fork.3`)
- Use npm Trusted Publishing (OIDC)
## OpenClaw Skill Sync
This repo includes a dedicated OpenClaw skill at:
- `skills/agent-browser-stealth/SKILL.md`
Local git `pre-push` hook auto-syncs skills before every push:
- `.husky/pre-push` -> `pnpm run clawhub:sync`
Manual sync command (same logic as hook):
```bash
pnpm run clawhub:sync
```
This uses your existing local ClawHub login session (no GitHub secret required).
Temporarily skip auto-sync for one push:
```bash
SKIP_CLAWHUB_SYNC=1 git push
```
This fork intentionally keeps command workflows close to upstream while concentrating custom behavior in stealth, policy, and anti-detection handling.
## License
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "agent-browser-stealth"
version = "0.14.0-fork.4"
version = "0.14.0-fork.5"
dependencies = [
"base64",
"dirs",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.14.0-fork.4"
version = "0.14.0-fork.5"
edition = "2021"
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
license = "Apache-2.0"
+22
View File
@@ -182,6 +182,19 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
nav_cmd["iosDevice"] = json!(device);
}
}
if let Some(ref risk_mode) = flags.risk_mode {
if matches!(risk_mode.as_str(), "off" | "warn" | "block") {
nav_cmd["riskMode"] = json!(risk_mode);
} else {
return Err(ParseError::InvalidValue {
message: format!(
"Invalid --risk-mode value: {} (expected off, warn, or block)",
risk_mode
),
usage: "open <url>",
});
}
}
Ok(nav_cmd)
}
"back" => Ok(json!({ "id": id, "action": "back" })),
@@ -2028,6 +2041,7 @@ mod tests {
annotate: false,
color_scheme: None,
download_path: None,
risk_mode: None,
}
}
@@ -2293,6 +2307,14 @@ mod tests {
assert_eq!(cmd["headers"]["Authorization"], "Bearer token");
}
#[test]
fn test_navigate_with_risk_mode() {
let mut flags = default_flags();
flags.risk_mode = Some("block".to_string());
let cmd = parse_command(&args("open https://example.com"), &flags).unwrap();
assert_eq!(cmd["riskMode"], "block");
}
#[test]
fn test_navigate_with_multiple_headers() {
let mut flags = default_flags();
+32 -1
View File
@@ -34,6 +34,7 @@ pub struct Config {
pub annotate: Option<bool>,
pub color_scheme: Option<String>,
pub download_path: Option<String>,
pub risk_mode: Option<String>,
}
impl Config {
@@ -68,6 +69,7 @@ impl Config {
annotate: other.annotate.or(self.annotate),
color_scheme: other.color_scheme.or(self.color_scheme),
download_path: other.download_path.or(self.download_path),
risk_mode: other.risk_mode.or(self.risk_mode),
}
}
}
@@ -134,6 +136,7 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
"--color-scheme",
"--channel",
"--download-path",
"--risk-mode",
];
let mut i = 0;
while i < args.len() {
@@ -204,6 +207,9 @@ pub struct Flags {
pub annotate: bool,
pub color_scheme: Option<String>,
pub download_path: Option<String>,
/// How verification/captcha detections are handled on navigation:
/// `off` (disable), `warn` (retry and warn), `block` (fail fast).
pub risk_mode: Option<String>,
// Track which launch-time options were explicitly passed via CLI
// (as opposed to being set only via environment variables)
@@ -285,6 +291,10 @@ pub fn parse_flags(args: &[String]) -> Flags {
.or(config.color_scheme),
download_path: env::var("AGENT_BROWSER_DOWNLOAD_PATH").ok()
.or(config.download_path),
risk_mode: env::var("AGENT_BROWSER_RISK_MODE")
.ok()
.or(config.risk_mode)
.map(|s| s.to_ascii_lowercase()),
cli_executable_path: false,
cli_extensions: false,
cli_state: false,
@@ -456,6 +466,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--risk-mode" => {
if let Some(s) = args.get(i + 1) {
flags.risk_mode = Some(s.to_ascii_lowercase());
i += 1;
}
}
"--config" => {
// Already handled by load_config(); skip the value
i += 1;
@@ -500,6 +516,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
"--session-name",
"--color-scheme",
"--download-path",
"--risk-mode",
"--config",
];
@@ -697,6 +714,18 @@ mod tests {
assert!(!flags.cli_download_path);
}
#[test]
fn test_parse_risk_mode_flag() {
let flags = parse_flags(&args("--risk-mode block open example.com"));
assert_eq!(flags.risk_mode.as_deref(), Some("block"));
}
#[test]
fn test_clean_args_removes_risk_mode() {
let cleaned = clean_args(&args("--risk-mode warn open example.com"));
assert_eq!(cleaned, vec!["open", "example.com"]);
}
#[test]
fn test_cli_multiple_flags_tracking() {
let flags = parse_flags(&args(
@@ -732,7 +761,8 @@ mod tests {
"allowFileAccess": true,
"cdp": "9222",
"autoConnect": true,
"headers": "{\"Auth\":\"token\"}"
"headers": "{\"Auth\":\"token\"}",
"riskMode": "block"
}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.headed, Some(true));
@@ -758,6 +788,7 @@ mod tests {
assert_eq!(config.cdp.as_deref(), Some("9222"));
assert_eq!(config.auto_connect, Some(true));
assert_eq!(config.headers.as_deref(), Some("{\"Auth\":\"token\"}"));
assert_eq!(config.risk_mode.as_deref(), Some("block"));
}
#[test]
+15
View File
@@ -153,6 +153,21 @@ fn main() {
return;
}
if let Some(ref risk_mode) = flags.risk_mode {
if !matches!(risk_mode.as_str(), "off" | "warn" | "block") {
let msg = format!(
"Invalid --risk-mode value: {} (expected off, warn, or block)",
risk_mode
);
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("{} {}", color::error_indicator(), msg);
}
exit(1);
}
}
if args.iter().any(|a| a == "--profile") {
let msg =
"Project policy: --profile is forbidden. Use your existing browser and --session-name for state persistence.";
+32
View File
@@ -25,6 +25,33 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) {
if let Some(warning) = data.get("warning").and_then(|v| v.as_str()) {
println!("{} {}", color::warning_indicator(), warning);
}
if let Some(risk_signals) = data.get("riskSignals").and_then(|v| v.as_array()) {
for signal in risk_signals {
let code = signal
.get("code")
.and_then(|v| v.as_str())
.unwrap_or("unknown_risk");
let source = signal.get("source").and_then(|v| v.as_str()).unwrap_or("unknown");
let evidence = signal
.get("evidence")
.and_then(|v| v.as_str())
.unwrap_or("-");
let confidence = signal.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.0);
println!(
"{} risk-signal code={} source={} evidence={} confidence={:.2}",
color::warning_indicator(),
code,
source,
evidence,
confidence
);
}
}
if let Some(warnings) = data.get("warnings").and_then(|v| v.as_array()) {
for warning in warnings.iter().filter_map(|v| v.as_str()) {
println!("{} {}", color::warning_indicator(), warning);
}
}
return;
}
println!("{}", url);
@@ -590,10 +617,12 @@ Global Options:
--json Output as JSON
--session <name> Use specific session
--headers <json> Set HTTP headers (scoped to this origin)
--risk-mode <mode> Risk handling for verify/captcha pages: off, warn, block
--headed Show browser window
Examples:
agent-browser open example.com
agent-browser --risk-mode block open example.com
agent-browser open https://github.com
agent-browser open localhost:3000
agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}'
@@ -2135,6 +2164,7 @@ Options:
Project default: require existing browser at localhost:9333 (no auto local fallback)
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
--risk-mode <mode> Verify/captcha handling: off, warn, block (or AGENT_BROWSER_RISK_MODE)
--session-name <name> Auto-save/restore session state (cookies, localStorage)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output
@@ -2186,6 +2216,7 @@ Environment:
AGENT_BROWSER_TIMEZONE Override auto-detected timezone (e.g., Asia/Taipei)
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads
AGENT_BROWSER_RISK_MODE Verify/captcha handling mode (off, warn, block)
AGENT_BROWSER_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)
@@ -2214,6 +2245,7 @@ Examples:
agent-browser --cdp 9222 snapshot # Connect via CDP port
agent-browser --auto-connect snapshot # Auto-discover running Chrome
agent-browser --color-scheme dark open example.com # Dark mode
agent-browser --risk-mode block open example.com # Block on verification/captcha pages
agent-browser --session-name myapp open example.com # Auto-save/restore state
Command Chaining:
+13 -2
View File
@@ -1,6 +1,6 @@
import { pageMetadata } from "@/lib/page-metadata"
import { pageMetadata } from '@/lib/page-metadata';
export const metadata = pageMetadata("commands")
export const metadata = pageMetadata('commands');
# Commands
@@ -8,6 +8,7 @@ export const metadata = pageMetadata("commands")
```bash
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block when verification/captcha interstitial is detected
agent-browser click <sel> # Click element (--new-tab to open in new tab)
agent-browser dblclick <sel> # Double-click
agent-browser fill <sel> <text> # Clear and fill
@@ -110,6 +111,16 @@ agent-browser wait --fn "condition" # Wait for JS condition
agent-browser wait --download [path] # Wait for download
```
## Risk Mode
Control how `open`/`navigate` handles verification or captcha interstitials:
```bash
agent-browser --risk-mode warn open https://example.com # default: retry and warn with riskSignals
agent-browser --risk-mode block open https://example.com # fail fast on detection
agent-browser --risk-mode off open https://example.com # disable detection/retry
```
## Downloads
```bash
+368 -43
View File
@@ -1,6 +1,6 @@
import { pageMetadata } from "@/lib/page-metadata"
import { pageMetadata } from '@/lib/page-metadata';
export const metadata = pageMetadata("configuration")
export const metadata = pageMetadata('configuration');
# Configuration
@@ -14,13 +14,39 @@ agent-browser checks two locations, merged in priority order:
<table>
<thead>
<tr><th>Priority</th><th>Location</th><th>Scope</th></tr>
<tr>
<th>Priority</th>
<th>Location</th>
<th>Scope</th>
</tr>
</thead>
<tbody>
<tr><td>1 (lowest)</td><td><code>~/.agent-browser/config.json</code></td><td>User-level defaults</td></tr>
<tr><td>2</td><td><code>./agent-browser.json</code></td><td>Project-level overrides</td></tr>
<tr><td>3</td><td><code>AGENT_BROWSER_*</code> env vars</td><td>Override config values</td></tr>
<tr><td>4 (highest)</td><td>CLI flags</td><td>Override everything</td></tr>
<tr>
<td>1 (lowest)</td>
<td>
<code>~/.agent-browser/config.json</code>
</td>
<td>User-level defaults</td>
</tr>
<tr>
<td>2</td>
<td>
<code>./agent-browser.json</code>
</td>
<td>Project-level overrides</td>
</tr>
<tr>
<td>3</td>
<td>
<code>AGENT_BROWSER_*</code> env vars
</td>
<td>Override config values</td>
</tr>
<tr>
<td>4 (highest)</td>
<td>CLI flags</td>
<td>Override everything</td>
</tr>
</tbody>
</table>
@@ -50,34 +76,229 @@ Every CLI flag can be set in the config file using its camelCase equivalent:
<table>
<thead>
<tr><th>Config Key</th><th>CLI Flag</th><th>Type</th></tr>
<tr>
<th>Config Key</th>
<th>CLI Flag</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr><td><code>headed</code></td><td><code>--headed</code></td><td>boolean</td></tr>
<tr><td><code>json</code></td><td><code>--json</code></td><td>boolean</td></tr>
<tr><td><code>full</code></td><td><code>--full, -f</code></td><td>boolean</td></tr>
<tr><td><code>debug</code></td><td><code>--debug</code></td><td>boolean</td></tr>
<tr><td><code>session</code></td><td><code>--session</code></td><td>string</td></tr>
<tr><td><code>sessionName</code></td><td><code>--session-name</code></td><td>string</td></tr>
<tr><td><code>executablePath</code></td><td><code>--executable-path</code></td><td>string</td></tr>
<tr><td><code>extensions</code></td><td><code>--extension</code></td><td>string[]</td></tr>
<tr><td><code>state</code></td><td><code>--state</code></td><td>string</td></tr>
<tr><td><code>proxy</code></td><td><code>--proxy</code></td><td>string</td></tr>
<tr><td><code>proxyBypass</code></td><td><code>--proxy-bypass</code></td><td>string</td></tr>
<tr><td><code>args</code></td><td><code>--args</code></td><td>string</td></tr>
<tr><td><code>userAgent</code></td><td><code>--user-agent</code></td><td>string</td></tr>
<tr><td><code>provider</code></td><td><code>-p, --provider</code></td><td>string</td></tr>
<tr><td><code>device</code></td><td><code>--device</code></td><td>string</td></tr>
<tr><td><code>ignoreHttpsErrors</code></td><td><code>--ignore-https-errors</code></td><td>boolean</td></tr>
<tr><td><code>allowFileAccess</code></td><td><code>--allow-file-access</code></td><td>boolean</td></tr>
<tr><td><code>cdp</code></td><td><code>--cdp</code></td><td>string</td></tr>
<tr><td><code>autoConnect</code></td><td><code>--auto-connect</code></td><td>boolean</td></tr>
<tr><td><code>colorScheme</code></td><td><code>--color-scheme</code></td><td>string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)</td></tr>
<tr><td><code>downloadPath</code></td><td><code>--download-path</code></td><td>string</td></tr>
<tr><td><code>headers</code></td><td><code>--headers</code></td><td>string (JSON)</td></tr>
<tr>
<td>
<code>headed</code>
</td>
<td>
<code>--headed</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>json</code>
</td>
<td>
<code>--json</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>full</code>
</td>
<td>
<code>--full, -f</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>debug</code>
</td>
<td>
<code>--debug</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>session</code>
</td>
<td>
<code>--session</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>sessionName</code>
</td>
<td>
<code>--session-name</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>executablePath</code>
</td>
<td>
<code>--executable-path</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>extensions</code>
</td>
<td>
<code>--extension</code>
</td>
<td>string[]</td>
</tr>
<tr>
<td>
<code>state</code>
</td>
<td>
<code>--state</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>proxy</code>
</td>
<td>
<code>--proxy</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>proxyBypass</code>
</td>
<td>
<code>--proxy-bypass</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>args</code>
</td>
<td>
<code>--args</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>userAgent</code>
</td>
<td>
<code>--user-agent</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>provider</code>
</td>
<td>
<code>-p, --provider</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>device</code>
</td>
<td>
<code>--device</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>ignoreHttpsErrors</code>
</td>
<td>
<code>--ignore-https-errors</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>allowFileAccess</code>
</td>
<td>
<code>--allow-file-access</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>cdp</code>
</td>
<td>
<code>--cdp</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>autoConnect</code>
</td>
<td>
<code>--auto-connect</code>
</td>
<td>boolean</td>
</tr>
<tr>
<td>
<code>colorScheme</code>
</td>
<td>
<code>--color-scheme</code>
</td>
<td>
string (<code>dark</code>, <code>light</code>, <code>no-preference</code>)
</td>
</tr>
<tr>
<td>
<code>downloadPath</code>
</td>
<td>
<code>--download-path</code>
</td>
<td>string</td>
</tr>
<tr>
<td>
<code>riskMode</code>
</td>
<td>
<code>--risk-mode</code>
</td>
<td>
string (<code>off</code>, <code>warn</code>, <code>block</code>)
</td>
</tr>
<tr>
<td>
<code>headers</code>
</td>
<td>
<code>--headers</code>
</td>
<td>string (JSON)</td>
</tr>
</tbody>
</table>
`riskMode` defaults to `warn` when unset.
## Common Configurations
### Local Development
@@ -146,21 +367,125 @@ These environment variables configure additional daemon and runtime behavior:
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
<tr>
<th>Variable</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_DOWNLOAD_PATH</code></td><td>Default directory for browser downloads.</td><td>(temp directory)</td></tr>
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM session encryption.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_STREAM_PORT</code></td><td>Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Default iOS device name for the <code>ios</code> provider.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Default iOS device UDID for the <code>ios</code> provider.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_DEBUG</code></td><td>Enable debug output (<code>1</code> to enable).</td><td>(disabled)</td></tr>
<tr>
<td>
<code>AGENT_BROWSER_AUTO_CONNECT</code>
</td>
<td>Auto-discover and connect to a running Chrome instance.</td>
<td>(disabled)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code>
</td>
<td>
Allow <code>file://</code> URLs to access local files.
</td>
<td>(disabled)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_COLOR_SCHEME</code>
</td>
<td>
Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).
</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_DOWNLOAD_PATH</code>
</td>
<td>Default directory for browser downloads.</td>
<td>(temp directory)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_RISK_MODE</code>
</td>
<td>
Verification/captcha handling mode (<code>off</code>, <code>warn</code>, <code>block</code>
).
</td>
<td>
<code>warn</code>
</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_DEFAULT_TIMEOUT</code>
</td>
<td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td>
<td>
<code>25000</code>
</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_SESSION_NAME</code>
</td>
<td>Auto-save/load state persistence name.</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code>
</td>
<td>Auto-delete saved session states older than N days.</td>
<td>
<code>30</code>
</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_ENCRYPTION_KEY</code>
</td>
<td>64-char hex key for AES-256-GCM session encryption.</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_STREAM_PORT</code>
</td>
<td>
Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).
</td>
<td>(disabled)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_IOS_DEVICE</code>
</td>
<td>
Default iOS device name for the <code>ios</code> provider.
</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_IOS_UDID</code>
</td>
<td>
Default iOS device UDID for the <code>ios</code> provider.
</td>
<td>(none)</td>
</tr>
<tr>
<td>
<code>AGENT_BROWSER_DEBUG</code>
</td>
<td>
Enable debug output (<code>1</code> to enable).
</td>
<td>(disabled)</td>
</tr>
</tbody>
</table>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.14.0-fork.4",
"version": "0.14.0-fork.5",
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module",
"main": "dist/daemon.js",
+29 -14
View File
@@ -51,6 +51,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle
```bash
# Navigation
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser --risk-mode block open <url> # Block if verification/captcha interstitial is detected
agent-browser close # Close browser
agent-browser --version # Show CLI version (fork builds include upstream/fork)
@@ -250,7 +251,19 @@ Override: `AGENT_BROWSER_LOCALE`, `AGENT_BROWSER_TIMEZONE` env vars.
### Captcha Detection & Auto-Retry
When a navigation lands on a captcha/verification page, the browser automatically retries up to 2 times with randomized backoff (3-7s). If detection persists, a warning is shown suggesting `--headed` mode or `--session-name` persistence.
When a navigation lands on a captcha/verification page, behavior is controlled by `--risk-mode` (or `AGENT_BROWSER_RISK_MODE`):
- `warn` (default): retry up to 2 times with randomized backoff (3-7s), then return warning plus structured `riskSignals`
- `block`: fail fast once a risk interstitial is detected
- `off`: disable this detection/retry path
Examples:
```bash
agent-browser --risk-mode warn open https://example.com
agent-browser --risk-mode block open https://example.com
AGENT_BROWSER_RISK_MODE=off agent-browser open https://example.com
```
### iOS Simulator (Mobile Safari)
@@ -390,6 +403,7 @@ agent-browser click @e2 # Click using ref from annotated screenshot
```
Use annotated screenshots when:
- The page has unlabeled icon buttons or visual-only elements
- You need to verify visual layout or styling
- Canvas or chart elements are present (invisible to text snapshots)
@@ -432,6 +446,7 @@ agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map
**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.
**Rules of thumb:**
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine
- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
- Programmatic/generated scripts -> use `eval -b` with base64
@@ -451,23 +466,23 @@ Priority (lowest to highest): `~/.agent-browser/config.json` < `./agent-browser.
## Deep-Dive Documentation
| Reference | When to Use |
|-----------|-------------|
| [references/commands.md](references/commands.md) | Full command reference with all options |
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
| Reference | When to Use |
| -------------------------------------------------------------------- | --------------------------------------------------------- |
| [references/commands.md](references/commands.md) | Full command reference with all options |
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
| [references/profiling.md](references/profiling.md) | Chrome DevTools profiling for performance analysis |
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
## Ready-to-Use Templates
| Template | Description |
|----------|-------------|
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
| Template | Description |
| ------------------------------------------------------------------------ | ----------------------------------- |
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
```bash
./templates/form-automation.sh https://example.com/form
+20 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { toAIFriendlyError } from './actions.js';
import { detectRiskSignals, toAIFriendlyError } from './actions.js';
describe('toAIFriendlyError', () => {
describe('element blocked by overlay', () => {
@@ -37,3 +37,22 @@ describe('toAIFriendlyError', () => {
});
});
});
describe('detectRiskSignals', () => {
it('should detect verification patterns from URL and title', () => {
const signals = detectRiskSignals(
'https://example.com/verify/captcha?scene=anti_bot',
'Just a moment...'
);
expect(signals.length).toBeGreaterThan(0);
expect(signals.some((s) => s.source === 'url' && s.code === 'captcha_interstitial')).toBe(true);
expect(
signals.some((s) => s.source === 'title' && s.code === 'verification_interstitial')
).toBe(true);
});
it('should return empty array for normal pages', () => {
const signals = detectRiskSignals('https://example.com/dashboard', 'Dashboard');
expect(signals).toEqual([]);
});
});
+109 -54
View File
@@ -127,7 +127,6 @@ import type {
DiffScreenshotCommand,
DiffUrlCommand,
Annotation,
NavigateData,
ScreenshotData,
EvaluateData,
DiffSnapshotData,
@@ -145,6 +144,8 @@ import type {
RecordingRestartData,
InputEventData,
StylesData,
RiskMode,
RiskSignal,
} from './types.js';
import { successResponse, errorResponse } from './protocol.js';
import { diffSnapshots, diffScreenshots } from './diff.js';
@@ -526,7 +527,7 @@ async function handleLaunch(
async function handleNavigate(
command: NavigateCommand,
browser: BrowserManager
): Promise<Response<NavigateData>> {
): Promise<Response> {
const page = browser.getPage();
// Set target URL for region auto-detection (locale/timezone)
@@ -545,71 +546,125 @@ async function handleNavigate(
waitUntil: command.waitUntil ?? 'load',
});
// Detect captcha/verification pages and retry with backoff
const finalUrl = page.url();
const title = await page.title();
const captchaDetected = isCaptchaPage(finalUrl, title);
if (captchaDetected) {
const maxRetries = 2;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const backoff = 3000 + Math.random() * 4000;
await page.waitForTimeout(Math.round(backoff));
await page.goto(command.url, {
waitUntil: command.waitUntil ?? 'load',
});
const retryUrl = page.url();
const retryTitle = await page.title();
if (!isCaptchaPage(retryUrl, retryTitle)) {
return successResponse(command.id, {
url: retryUrl,
title: retryTitle,
});
}
}
// All retries exhausted -- return the page as-is with a warning
const riskMode: RiskMode = command.riskMode ?? 'warn';
if (riskMode === 'off') {
return successResponse(command.id, {
url: page.url(),
title: await page.title(),
warning:
'Captcha/verification page detected. Try --headed mode or use --session-name for state persistence.',
} as NavigateData);
});
}
// Detect risk interstitials (captcha/verification) and handle by risk mode.
const finalUrl = page.url();
const title = await page.title();
let encounteredSignals = detectRiskSignals(finalUrl, title);
if (encounteredSignals.length === 0) {
return successResponse(command.id, {
url: finalUrl,
title,
});
}
if (riskMode === 'block') {
const first = encounteredSignals[0];
return errorResponse(
command.id,
`Navigation blocked by risk-mode=block: ${first.code} (${first.source}="${first.evidence}")`
);
}
const maxRetries = 2;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const backoff = 3000 + Math.random() * 4000;
await page.waitForTimeout(Math.round(backoff));
await page.goto(command.url, {
waitUntil: command.waitUntil ?? 'load',
});
const retryUrl = page.url();
const retryTitle = await page.title();
const retrySignals = detectRiskSignals(retryUrl, retryTitle);
if (retrySignals.length === 0) {
return successResponse(command.id, {
url: retryUrl,
title: retryTitle,
warning:
'Risk interstitial detected and recovered after retry. Review riskSignals for evidence.',
riskSignals: encounteredSignals,
});
}
encounteredSignals = mergeRiskSignals(encounteredSignals, retrySignals);
}
// All retries exhausted -- return the page as-is with a warning and evidence.
return successResponse(command.id, {
url: finalUrl,
title,
url: page.url(),
title: await page.title(),
warning:
'Captcha/verification page detected. Try --headed mode or use --session-name for state persistence.',
riskSignals: encounteredSignals,
});
}
function isCaptchaPage(url: string, title: string): boolean {
function mergeRiskSignals(current: RiskSignal[], next: RiskSignal[]): RiskSignal[] {
const merged = new Map<string, RiskSignal>();
for (const signal of [...current, ...next]) {
const key = `${signal.code}|${signal.source}|${signal.evidence}`;
if (!merged.has(key) || (merged.get(key)?.confidence ?? 0) < signal.confidence) {
merged.set(key, signal);
}
}
return [...merged.values()];
}
/**
* Detect verification/captcha interstitials and return structured risk evidence.
*/
export function detectRiskSignals(url: string, title: string): RiskSignal[] {
const lowerUrl = url.toLowerCase();
const lowerTitle = title.toLowerCase();
const captchaPatterns = [
'/verify/captcha',
'/captcha',
'/challenge',
'scene=crawler',
'scene=anti_bot',
'recaptcha',
'hcaptcha',
const urlPatterns: Array<{ pattern: string; code: string; confidence: number }> = [
{ pattern: '/verify/captcha', code: 'captcha_interstitial', confidence: 0.98 },
{ pattern: '/captcha', code: 'captcha_interstitial', confidence: 0.95 },
{ pattern: '/challenge', code: 'verification_interstitial', confidence: 0.93 },
{ pattern: 'scene=crawler', code: 'bot_challenge', confidence: 0.99 },
{ pattern: 'scene=anti_bot', code: 'bot_challenge', confidence: 0.99 },
{ pattern: 'recaptcha', code: 'captcha_interstitial', confidence: 0.97 },
{ pattern: 'hcaptcha', code: 'captcha_interstitial', confidence: 0.97 },
];
const titlePatterns = [
'verify',
'captcha',
'challenge',
'attention required',
'just a moment',
'checking your browser',
'access denied',
'驗證',
'验证',
'人机验证',
const titlePatterns: Array<{ pattern: string; code: string; confidence: number }> = [
{ pattern: 'verify', code: 'verification_interstitial', confidence: 0.78 },
{ pattern: 'captcha', code: 'captcha_interstitial', confidence: 0.9 },
{ pattern: 'challenge', code: 'verification_interstitial', confidence: 0.8 },
{ pattern: 'attention required', code: 'verification_interstitial', confidence: 0.96 },
{ pattern: 'just a moment', code: 'verification_interstitial', confidence: 0.95 },
{ pattern: 'checking your browser', code: 'verification_interstitial', confidence: 0.97 },
{ pattern: 'access denied', code: 'access_gate', confidence: 0.86 },
{ pattern: '驗證', code: 'verification_interstitial', confidence: 0.88 },
{ pattern: '验证', code: 'verification_interstitial', confidence: 0.88 },
{ pattern: '人机验证', code: 'captcha_interstitial', confidence: 0.95 },
];
return (
captchaPatterns.some((p) => lowerUrl.includes(p)) ||
titlePatterns.some((p) => lowerTitle.includes(p))
);
const signals: RiskSignal[] = [];
for (const item of urlPatterns) {
if (lowerUrl.includes(item.pattern)) {
signals.push({
code: item.code,
source: 'url',
evidence: item.pattern,
confidence: item.confidence,
});
}
}
for (const item of titlePatterns) {
if (lowerTitle.includes(item.pattern)) {
signals.push({
code: item.code,
source: 'title',
evidence: item.pattern,
confidence: item.confidence,
});
}
}
return mergeRiskSignals([], signals);
}
function bezierPoint(t: number, p0: number, p1: number, p2: number, p3: number): number {
+27
View File
@@ -44,11 +44,38 @@ describe('parseCommand', () => {
}
});
it('should parse navigate with riskMode', () => {
const result = parseCommand(
cmd({
id: '1',
action: 'navigate',
url: 'https://example.com',
riskMode: 'block',
})
);
expect(result.success).toBe(true);
if (result.success) {
expect(result.command.riskMode).toBe('block');
}
});
it('should reject navigate without url', () => {
const result = parseCommand(cmd({ id: '1', action: 'navigate' }));
expect(result.success).toBe(false);
});
it('should reject navigate with invalid riskMode', () => {
const result = parseCommand(
cmd({
id: '1',
action: 'navigate',
url: 'https://example.com',
riskMode: 'invalid',
})
);
expect(result.success).toBe(false);
});
it('should parse back command', () => {
const result = parseCommand(cmd({ id: '1', action: 'back' }));
expect(result.success).toBe(true);
+1
View File
@@ -59,6 +59,7 @@ const navigateSchema = baseCommandSchema.extend({
url: z.string().min(1),
waitUntil: z.enum(['load', 'domcontentloaded', 'networkidle']).optional(),
headers: z.record(z.string()).optional(),
riskMode: z.enum(['off', 'warn', 'block']).optional(),
});
const clickSchema = baseCommandSchema.extend({
+13
View File
@@ -6,6 +6,15 @@ export interface BaseCommand {
action: string;
}
export type RiskMode = 'off' | 'warn' | 'block';
export interface RiskSignal {
code: string;
source: 'url' | 'title';
evidence: string;
confidence: number;
}
// Action-specific command types
export interface LaunchCommand extends BaseCommand {
action: 'launch';
@@ -41,6 +50,8 @@ export interface NavigateCommand extends BaseCommand {
url: string;
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle';
headers?: Record<string, string>;
// off: skip detection/retry, warn: retry then return warning+riskSignals, block: fail fast
riskMode?: RiskMode;
}
export interface ClickCommand extends BaseCommand {
@@ -1074,6 +1085,8 @@ export interface NavigateData {
url: string;
title: string;
warning?: string;
// Structured evidence emitted when verification/captcha patterns are detected.
riskSignals?: RiskSignal[];
}
export interface Annotation {