Compare commits

..
Author SHA1 Message Date
leeguooooo d1fbdaadeb chore(release): 0.27.0-fork.30 — stealth: navigator overrides on prototype, not instance
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
rebrowser's navigatorWebdriver probe checks Object.getOwnPropertyNames(navigator)
== [] (real Chrome keeps navigator members on Navigator.prototype). The launch-mode
stealth script defined language/languages/userAgentData/contacts as instance
own-properties, leaking them as an automation tell.

- add __abRedefineNavProto(name, getterImpl): redefines a navigator member on the
  PROTOTYPE with a native-masked getter toString, then deletes any instance shadow
  (mirrors the existing vendor patch). Falls back to instance only if proto is locked.
- convert language/languages/userAgentData to it; make the contacts block prototype-first.

After: Object.getOwnPropertyNames(navigator) == [], values intact, getters native,
rebrowser navigatorWebdriver 🟢, runtimeEnableLeak/pwInitScripts 🟢, sannysoft 0 fails.
2026-06-10 11:35:35 +09:00
leeguooooo 839aaa5586 chore(release): 0.27.0-fork.29 — plugin overflowTest fix, popup-free auto-connect, ab-connect rebrand+icon, README
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
- stealth(plugins): stop overwriting real native navigator.plugins in headed
  mode (the JS fake had a non-native item(), broken uint32 wrap → incolumitas
  overflowTest FAIL, and an anachronistic Native Client plugin). Leave native
  plugins untouched when present; modernize the headless-escape fallback to the
  real 5 PDF-viewer set with masked-native item()/namedItem().
- connect: auto_connect_cdp() now prefers the dialog-free ab-connect relay over
  the raw :9222 CDP port, so Chrome 136+'s "Allow remote debugging?" consent
  modal no longer fires when the extension relay is live. Gated by a bare-TCP
  relay_is_live() probe (+3 unit tests).
- extension: rename ab-connect to "agent-browser-stealth" + new stealth icon set
  (16/32/48/128).
- docs(README): hero/shield/fingerprint images, expanded detector results
  (CreepJS 0% stealth, incolumitas all-OK, BrowserScan CDP-clean), and a
  "Verify it yourself" section. .gitignore: allow assets/ + extension icons.
2026-06-10 11:17:41 +09:00
leeguooooo a7f9c24fdb chore(release): 0.27.0-fork.28 — skill docs (headed default, tab groups) embedded
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
2026-06-10 09:58:35 +09:00
leeguooooo 42ade7b4e8 docs(skill): headed-default/headless-forbidden + per-session tab groups + stealth ranking
Update the served skill (skill-data/core, embedded into the binary) for tonight's
changes: --headed is the default and headless is FORBIDDEN (was wrongly 'default
is headless'); each --session on the extension-connect path gets its own colored
tab group with no cross-talk; anti-detection ranking real-Chrome(extension) >
headed-launch > headless(forbidden). Needs a rebuild so standalone installs'
embedded skill reflects it.
2026-06-10 09:58:33 +09:00
17 changed files with 205 additions and 51 deletions
+4
View File
@@ -38,6 +38,10 @@ __pycache__/
*.webm *.webm
test/e2e/.dogfood-output/ test/e2e/.dogfood-output/
# ...but these are real repo assets, not test artifacts — keep them tracked
!assets/*.png
!extensions/ab-connect/icons/*.png
# Package manager # Package manager
package-lock.json package-lock.json
yarn.lock yarn.lock
+26 -4
View File
@@ -1,11 +1,15 @@
# agent-browser-stealth # agent-browser-stealth
![agent-browser-stealth](assets/hero.png)
Stealth fork of [agent-browser](https://github.com/vercel-labs/agent-browser) — connects to your real Chrome, shares your login sessions, and is undetectable by anti-bot systems. Stealth fork of [agent-browser](https://github.com/vercel-labs/agent-browser) — connects to your real Chrome, shares your login sessions, and is undetectable by anti-bot systems.
For basic usage, commands, and API reference, see the [upstream documentation](https://github.com/vercel-labs/agent-browser). For basic usage, commands, and API reference, see the [upstream documentation](https://github.com/vercel-labs/agent-browser).
## Why this fork? ## Why this fork?
<img src="assets/fingerprint.png" alt="real but undetectable fingerprint" width="300" align="right" />
**agent-browser** launches a fresh browser with an empty profile. You need to log in again, and websites can detect it's automated. **agent-browser** launches a fresh browser with an empty profile. You need to log in again, and websites can detect it's automated.
**agent-browser-stealth** connects to your existing Chrome. Your cookies, sessions, and browser fingerprint are all real — because it IS your real browser. **agent-browser-stealth** connects to your existing Chrome. Your cookies, sessions, and browser fingerprint are all real — because it IS your real browser.
@@ -115,6 +119,8 @@ In CI environments, standalone mode is used automatically.
## Anti-detection ## Anti-detection
<img src="assets/shield.png" alt="stealth shield" width="320" align="right" />
When connected to your real Chrome, we inject **zero** JavaScript patches. Your browser's fingerprint is completely genuine. The guiding rule is **native CDP/Chrome overrides over JS lies** — a re-defined getter is itself detectable; a native override isn't. When connected to your real Chrome, we inject **zero** JavaScript patches. Your browser's fingerprint is completely genuine. The guiding rule is **native CDP/Chrome overrides over JS lies** — a re-defined getter is itself detectable; a native override isn't.
- `navigator.webdriver = false` via `Emulation.setAutomationOverride` (native, undetectable by CreepJS-style lie tests). - `navigator.webdriver = false` via `Emulation.setAutomationOverride` (native, undetectable by CreepJS-style lie tests).
@@ -124,11 +130,27 @@ When connected to your real Chrome, we inject **zero** JavaScript patches. Your
| Test site | Result | | Test site | Result |
|---|---| |---|---|
| [CreepJS](https://abrahamjuliot.github.io/creepjs/) | 0% stealth, 0% headless | | [CreepJS](https://abrahamjuliot.github.io/creepjs/) | **0% stealth · 0% headless** (no override traces at all) |
| [bot.sannysoft.com](https://bot.sannysoft.com) | All green | | [bot.incolumitas.com](https://bot.incolumitas.com/) | all checks OK — `overflowTest`, `overrideTest`, `puppeteerExtraStealthUsed`, worker consistency |
| [Cloudflare Turnstile](https://nowsecure.nl) | Passed | | [bot.sannysoft.com](https://bot.sannysoft.com) | all green |
| [BrowserScan](https://www.browserscan.net/bot-detection) | Webdriver · User-Agent · CDP all clean |
| [Cloudflare Turnstile](https://nowsecure.nl) | passed |
When using `--launch` mode (standalone browser), a full suite of 32 stealth patches is applied for headless Chrome. `0% stealth` on CreepJS is the key number: because the connect path patches **nothing**, there is no override for a lie-detector to catch. (Dashboards that read `navigator.languages` order or IP geolocation may show a soft "navigator"/"location" flag — that tracks *your real Chrome's* language list and network, not an automation tell.)
When using `--launch` mode (standalone browser), a full suite of stealth patches is applied instead, and it still passes the suite above.
### Verify it yourself
Don't take our word for it — point your connected Chrome at the toughest public detectors and compare:
- **[CreepJS](https://abrahamjuliot.github.io/creepjs/)** — the most thorough fingerprint / lie detector
- **[bot.incolumitas.com](https://bot.incolumitas.com/)** — behavioral + fingerprint scoring with a public methodology
- **[BrowserScan](https://www.browserscan.net/bot-detection)** — Webdriver / User-Agent / CDP / Navigator
- **[bot.sannysoft.com](https://bot.sannysoft.com)** — the classic automation-marker checklist
- **[pixelscan.net](https://pixelscan.net/)** · **[iphey.com](https://iphey.com/)** — consistency & identity
We deliberately **don't ship our own bot detector** — the strongest, most honest benchmark is the market's best detectors run against your real browser.
### Tuning knobs (environment variables) ### Tuning knobs (environment variables)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]] [[package]]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.27.0-fork.27" version = "0.27.0-fork.30"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "agent-browser-stealth" name = "agent-browser-stealth"
version = "0.27.0-fork.27" version = "0.27.0-fork.30"
edition = "2021" edition = "2021"
description = "Fast browser automation CLI for AI agents" description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0" license = "Apache-2.0"
+67
View File
@@ -773,6 +773,26 @@ fn running_process_cmdlines() -> Option<Vec<String>> {
} }
pub async fn auto_connect_cdp() -> Result<String, String> { pub async fn auto_connect_cdp() -> Result<String, String> {
// Prefer the dialog-free `ab-connect` extension relay when it is live.
// The relay drives the user's REAL Chrome via the extension's
// `chrome.debugger` permission, which — unlike a raw `--remote-debugging-port`
// CDP attach — never triggers Chrome 136+'s per-connection
// "Allow remote debugging?" consent modal. The native-messaging host writes
// ~/.agent-browser/relay-cdp-url while connected and removes it on exit, so a
// present URL means the relay is up. This must win over the DevToolsActivePort
// / :9222 probes below: if the user's Chrome happens to also be listening on a
// debug port, attaching there would pop the consent dialog and defeat the
// whole zero-interaction extension path.
if let Some(relay) = crate::connect::relay_url() {
// The relay is a local CDP-over-WS endpoint we connect to like Chrome.
// A bare TCP liveness check (no WS upgrade) confirms it is actually
// accepting before we commit, mirroring the consent-free probe used for
// DevToolsActivePort.
if relay_is_live(&relay).await {
return Ok(relay);
}
}
let user_data_dirs = get_chrome_user_data_dirs(); let user_data_dirs = get_chrome_user_data_dirs();
for dir in &user_data_dirs { for dir in &user_data_dirs {
@@ -850,6 +870,22 @@ async fn tcp_port_alive(port: u16) -> bool {
) )
} }
/// Consent-free liveness for the `ab-connect` relay ws URL (`ws://127.0.0.1:<port>/…`).
/// Parses the port and does a bare TCP connect — a stale relay-cdp-url file
/// (host exited without cleanup) must not divert auto-connect away from the
/// working port path.
async fn relay_is_live(ws_url: &str) -> bool {
let port = ws_url
.strip_prefix("ws://")
.and_then(|rest| rest.split('/').next())
.and_then(|hostport| hostport.rsplit(':').next())
.and_then(|p| p.parse::<u16>().ok());
match port {
Some(p) => tcp_port_alive(p).await,
None => false,
}
}
/// Returns the default Chrome user-data directory paths for the current platform. /// Returns the default Chrome user-data directory paths for the current platform.
/// Includes Chrome, Chrome Canary, Chromium, and Brave. /// Includes Chrome, Chrome Canary, Chromium, and Brave.
pub fn get_chrome_user_data_dirs() -> Vec<PathBuf> { pub fn get_chrome_user_data_dirs() -> Vec<PathBuf> {
@@ -2200,4 +2236,35 @@ mod tests {
let result = resolve_cdp_from_active_port(port, "/devtools/browser/dead").await; let result = resolve_cdp_from_active_port(port, "/devtools/browser/dead").await;
assert!(result.is_err(), "should fail when nothing is listening"); assert!(result.is_err(), "should fail when nothing is listening");
} }
#[tokio::test]
async fn test_relay_is_live_true_when_listening() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let url = format!("ws://127.0.0.1:{}/abc-guid", port);
assert!(
relay_is_live(&url).await,
"relay_is_live should be true while the port is accepting"
);
}
#[tokio::test]
async fn test_relay_is_live_false_when_dead() {
// Bind to grab a free port, then drop so nothing is listening.
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let url = format!("ws://127.0.0.1:{}/abc-guid", port);
assert!(
!relay_is_live(&url).await,
"relay_is_live must be false for a stale relay-cdp-url (host exited)"
);
}
#[tokio::test]
async fn test_relay_is_live_false_on_malformed_url() {
assert!(!relay_is_live("not-a-ws-url").await);
assert!(!relay_is_live("ws://127.0.0.1/no-port").await);
assert!(!relay_is_live("ws://127.0.0.1:notaport/x").await);
}
} }
+88 -39
View File
@@ -1,4 +1,31 @@
const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0 }; const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0 };
// Redefine a navigator property on its PROTOTYPE (Navigator / WorkerNavigator),
// the way real Chrome exposes these — as prototype getters, NOT instance own
// properties. Adding an own property to the `navigator` instance is itself a
// detectable automation tell: real Chrome's `Object.getOwnPropertyNames(navigator)`
// is empty, so any name we leave on the instance is caught by rebrowser's
// `navigatorWebdriver` probe and similar checks. We mirror the proven `vendor`
// patch below: define on the prototype, native-mask the getter's toString, then
// delete any instance shadow. Falls back to an instance define only if the
// prototype is locked. (A top-level `const` like this is script-scoped, not a
// `window` property, so it does not leak — same as `__abStealth` above.)
const __abRedefineNavProto = (name, getterImpl) => {
try {
const proto = Object.getPrototypeOf(navigator);
const nativeGet = Object.getOwnPropertyDescriptor(proto, name) && Object.getOwnPropertyDescriptor(proto, name).get;
const getter = function () { return getterImpl(); };
if (nativeGet) {
Object.defineProperty(getter, 'name', { value: 'get ' + name, configurable: true });
Object.defineProperty(getter, 'toString', { value: () => nativeGet.toString(), configurable: true, writable: true });
}
Object.defineProperty(proto, name, { get: getter, configurable: true, enumerable: true });
try { delete navigator[name]; } catch (e) {}
return true;
} catch (e) {
try { Object.defineProperty(navigator, name, { get: () => getterImpl(), configurable: true }); } catch (e2) {}
return false;
}
};
(function(){ (function(){
// Prefer the CDP-level automation override (Emulation.setAutomationOverride), // Prefer the CDP-level automation override (Emulation.setAutomationOverride),
// which makes navigator.webdriver report `false` NATIVELY — undetectable by // which makes navigator.webdriver report `false` NATIVELY — undetectable by
@@ -354,18 +381,8 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
const config = (typeof __abStealth === 'object' && __abStealth) ? __abStealth : null; const config = (typeof __abStealth === 'object' && __abStealth) ? __abStealth : null;
if (!config || !Array.isArray(config.languages) || config.languages.length === 0) return; if (!config || !Array.isArray(config.languages) || config.languages.length === 0) return;
const locale = typeof config.locale === 'string' ? config.locale : config.languages[0]; const locale = typeof config.locale === 'string' ? config.locale : config.languages[0];
try { __abRedefineNavProto('language', () => locale);
Object.defineProperty(navigator, 'language', { __abRedefineNavProto('languages', () => config.languages.slice());
get: () => locale,
configurable: true,
});
} catch {}
try {
Object.defineProperty(navigator, 'languages', {
get: () => config.languages.slice(),
configurable: true,
});
} catch {}
})(); })();
(function(){ (function(){
const ua = String(navigator.userAgent || ''); const ua = String(navigator.userAgent || '');
@@ -394,6 +411,24 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
defineVendor(navigator); defineVendor(navigator);
})(); })();
(function(){ (function(){
// Native > JS lies: a real headed Chrome already exposes the correct, fully
// native navigator.plugins (5 PDF-viewer aliases, a native item() that does
// the WebIDL uint32-index wrap, length on the prototype). Overriding that
// with a JS fake is strictly worse — it ships a non-native item() whose
// .toString() reveals the patch, breaks the uint32 wrap (incolumitas
// overflowTest), and pins an anachronistic "Native Client" plugin that modern
// Chrome removed. Since this fork forbids headless and always launches headed,
// the native plugins are present, so we leave them alone. We only fall back to
// a synthetic list when native plugins are genuinely empty (e.g. the
// discouraged AGENT_BROWSER_ALLOW_HEADLESS escape on old headless).
try {
const np = navigator.plugins;
const itemNative =
np && typeof np.item === 'function' &&
/\[native code\]/.test(Function.prototype.toString.call(np.item));
if (np && np.length > 0 && itemNative) return;
} catch (e) {}
const makeMimeType = (type, suffixes, description) => { const makeMimeType = (type, suffixes, description) => {
const mime = Object.create(MimeType.prototype); const mime = Object.create(MimeType.prototype);
Object.defineProperties(mime, { Object.defineProperties(mime, {
@@ -427,40 +462,54 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
return plugin; return plugin;
}; };
// Make a fake method masquerade as native: name + `[native code]` toString.
const maskNative = (fn, name) => {
Object.defineProperty(fn, 'name', { value: name, configurable: true });
Object.defineProperty(fn, 'toString', {
value: () => `function ${name}() { [native code] }`,
configurable: true,
writable: true,
});
return fn;
};
// Modern Chrome (since ~v109) exposes exactly these 5 PDF-viewer aliases and
// two mimeTypes (application/pdf, text/pdf). Native Client was removed years
// ago, so it must NOT appear. Each plugin carries both mimeTypes.
const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format'); const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format');
const chromePdfMime = makeMimeType( const textPdfMime = makeMimeType('text/pdf', 'pdf', 'Portable Document Format');
'application/x-google-chrome-pdf', const mimes = [pdfMime, textPdfMime];
'pdf',
'Portable Document Format'
);
const naclMime = makeMimeType('application/x-nacl', '', 'Native Client Executable');
const pnaclMime = makeMimeType('application/x-pnacl', '', 'Portable Native Client Executable');
const plugins = [ const plugins = [
makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', [chromePdfMime]), 'PDF Viewer',
makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', [pdfMime]), 'Chrome PDF Viewer',
makePlugin('Native Client', '', 'internal-nacl-plugin', [naclMime, pnaclMime]), 'Chromium PDF Viewer',
]; 'Microsoft Edge PDF Viewer',
'WebKit built-in PDF',
].map((name) => makePlugin(name, 'Portable Document Format', 'internal-pdf-viewer', mimes));
const pluginArray = Object.create(PluginArray.prototype); const pluginArray = Object.create(PluginArray.prototype);
plugins.forEach((p, i) => { plugins.forEach((p, i) => {
pluginArray[i] = p; pluginArray[i] = p;
pluginArray[p.name] = p; pluginArray[p.name] = p;
}); });
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length }); Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
pluginArray.item = (i) => plugins[i] || null; // `i >>> 0` replicates the WebIDL unsigned-long index coercion, so
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) || null; // item(2**32) wraps to item(0) like the real native PluginArray.item.
pluginArray.refresh = () => {}; pluginArray.item = maskNative((i) => plugins[i >>> 0] || null, 'item');
pluginArray.namedItem = maskNative((name) => plugins.find(p => p.name === name) || null, 'namedItem');
pluginArray.refresh = maskNative(() => {}, 'refresh');
pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; }; pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
const mimeTypes = [chromePdfMime, pdfMime, naclMime, pnaclMime]; const mimeTypes = [pdfMime, textPdfMime];
const mimeTypeArray = Object.create(MimeTypeArray.prototype); const mimeTypeArray = Object.create(MimeTypeArray.prototype);
mimeTypes.forEach((m, i) => { mimeTypes.forEach((m, i) => {
mimeTypeArray[i] = m; mimeTypeArray[i] = m;
mimeTypeArray[m.type] = m; mimeTypeArray[m.type] = m;
}); });
Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length }); Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length });
mimeTypeArray.item = (i) => mimeTypes[i] || null; mimeTypeArray.item = maskNative((i) => mimeTypes[i >>> 0] || null, 'item');
mimeTypeArray.namedItem = (name) => mimeTypes.find(m => m.type === name) || null; mimeTypeArray.namedItem = maskNative((name) => mimeTypes.find(m => m.type === name) || null, 'namedItem');
mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; }; mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; };
Object.defineProperty(navigator, 'plugins', { Object.defineProperty(navigator, 'plugins', {
@@ -1023,10 +1072,15 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
return false; return false;
} }
}; };
if (defineContacts(navigator)) return; // Prototype-first (like the vendor patch): real Chrome exposes navigator
try { // members on the prototype, not as instance own properties. Define on the
defineContacts(Object.getPrototypeOf(navigator)); // prototype and remove any instance shadow so Object.getOwnPropertyNames(navigator)
} catch {} // stays empty; fall back to the instance only if the prototype is locked.
if (defineContacts(Object.getPrototypeOf(navigator))) {
try { delete navigator.contacts; } catch {}
return;
}
defineContacts(navigator);
})(); })();
(function(){ (function(){
const ContentIndexCtor = typeof ContentIndex === 'function' const ContentIndexCtor = typeof ContentIndex === 'function'
@@ -1233,12 +1287,7 @@ const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLCon
} }
return values; return values;
}; };
try { __abRedefineNavProto('userAgentData', () => patched);
Object.defineProperty(navigator, 'userAgentData', {
get: () => patched,
configurable: true,
});
} catch {}
})(); })();
(function(){ (function(){
const ua = navigator.userAgent; const ua = navigator.userAgent;
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "agent-browser connect", "name": "agent-browser-stealth",
"version": "0.4.0", "version": "0.4.0",
"description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.", "description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
@@ -24,6 +24,6 @@
"type": "module" "type": "module"
}, },
"action": { "action": {
"default_title": "agent-browser connect" "default_title": "agent-browser-stealth"
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "agent-browser-stealth", "name": "agent-browser-stealth",
"version": "0.27.0-fork.27", "version": "0.27.0-fork.30",
"description": "Browser automation CLI for AI agents \u2014 stealth fork with anti-detection", "description": "Browser automation CLI for AI agents stealth fork with anti-detection",
"type": "module", "type": "module",
"packageManager": "pnpm@11.1.3", "packageManager": "pnpm@11.1.3",
"files": [ "files": [
+12 -1
View File
@@ -57,6 +57,15 @@ GUI-automation tool** like the `cua-driver` skill — see
`agent-browser extension connect`. After that it's zero-confirmation, zero-token `agent-browser extension connect`. After that it's zero-confirmation, zero-token
CLI. Use `--launch` instead when a fresh, isolated browser is fine. CLI. Use `--launch` instead when a fresh, isolated browser is fine.
Each `--session` that connects gets its **own colored Chrome tab group** (named
after the session) and drives only its own tabs — multiple agents share the one
real browser without cross-talk, and the user's own tabs are never grouped. CDP
drives the page without moving the user's mouse/keyboard, so it doesn't fight
them for control. **Anti-detection ranking: this real logged-in Chrome (extension
connect) > a headed launched browser > headless (forbidden).** A genuine human
browser has no headless/automation tells at all, so prefer it for anything
anti-bot-sensitive.
## Two ways to drive a page — and when to drop to `eval` ## Two ways to drive a page — and when to drop to `eval`
You have a **real Chrome with the user's DOM**. Two layers, mix them freely: You have a **real Chrome with the user's DOM**. Two layers, mix them freely:
@@ -506,7 +515,9 @@ and [references/authentication.md](references/authentication.md).
```bash ```bash
--session <name> # isolated browser session --session <name> # isolated browser session
--json # JSON output (for machine parsing) --json # JSON output (for machine parsing)
--headed # show the window (default is headless) --headed # default & always-on for stealth — headless is FORBIDDEN
# (a bot tell: creepjs flags ~33% headless vs 0% headed).
# Display-less servers only: AGENT_BROWSER_ALLOW_HEADLESS=1
--auto-connect # connect to an already-running Chrome --auto-connect # connect to an already-running Chrome
--cdp <port> # connect to a specific CDP port --cdp <port> # connect to a specific CDP port
--profile <name|path> # use a Chrome profile (login state survives) --profile <name|path> # use a Chrome profile (login state survives)
+2 -1
View File
@@ -302,7 +302,8 @@ agent-browser state load auth.json # Restore saved state
```bash ```bash
agent-browser --session <name> ... # Isolated browser session agent-browser --session <name> ... # Isolated browser session
agent-browser --json ... # JSON output for parsing agent-browser --json ... # JSON output for parsing
agent-browser --headed ... # Show browser window (not headless) agent-browser --headed ... # Default & always-on (stealth). Headless is FORBIDDEN
# (bot tell); display-less servers: AGENT_BROWSER_ALLOW_HEADLESS=1
agent-browser --full ... # Full page screenshot (-f) agent-browser --full ... # Full page screenshot (-f)
agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol
agent-browser -p <provider> ... # Cloud browser provider (--provider) agent-browser -p <provider> ... # Cloud browser provider (--provider)