Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] - 2026-10-04

### Added

- **Real fingerprint mode for persistent profiles.** `fingerprint: { mode: 'real' }` in the library,
`--fingerprint real` in the CLI, `fingerprint: "real"` in MCP, on create and update. A real
profile launches with Chrome's own identity: no generated fingerprint, no user agent or Client
Hints override, no injected script, no auto-attach, no kernel or anti-detect flags, and no
timezone or locale unless the profile sets one. Meant for accounts signed in by hand, where a
spoofed fingerprint triggers new-device checks. It refuses `engine: "kernel"` and `"inject"` with
`INVALID_CONFIG`, and rejects `platform`. Existing profiles are unchanged.
Real launches keep Chrome's sandbox, Safe Browsing, popup blocking and updates on (no `--no-sandbox`).
- `browser status` reports each browser's `engine`, and `engine` can now be `real` in launch results
and lock files. `profile list` shows a Fingerprint column.

### Changed

- `ResolvedEngine` gains `'real'`. Code that switches on `LaunchResult.engine` should handle it.
- A real profile gets no default timezone at create time, and switching a profile to real drops its
stored timezone unless the same update sets one. Generated profiles still default to
`America/New_York` (or `defaultTimezone`).

## [0.4.0] - 2026-10-04

### Added
Expand Down
34 changes: 33 additions & 1 deletion docs/anti-detect.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
# Anti-detect

What a profile actually hides, how it is applied, and where the limits are. No extension is
installed. Two engines:
installed. Two engines, plus a real mode that uses neither:

| Engine | When | How |
|--------|------|-----|
| `kernel` | The executable is a [fingerprint-chromium](https://github.com/adryfish/fingerprint-chromium) build, or `engine: "kernel"` | Chromium spoofs the fingerprint from a per-profile seed (`--fingerprint`). No JavaScript hooks are installed. |
| `inject` | Stock Google Chrome, or `engine: "inject"` | Scripts and CDP overrides, applied when the browser launches. |
| `real` | The profile has `fingerprint: { mode: "real" }` | Nothing. Chrome runs with its own identity. See [Real mode](#real-mode). |

`engine` defaults to `auto`: kernel when the binary's switch table (the framework on macOS,
`chrome.dll` on Windows) contains `fingerprint-platform`, inject otherwise. With no `chromePath` and no `CHROMIUM_PATH`/`CHROME_PATH`, a kernel build at
`~/.aitofy/browser-profiles/kernel/` or a `Chromium.app` that has the switch is preferred over
Google Chrome.

## Real mode

For your own accounts, signed in by hand once (Google Cloud, Firebase, Play Console, Cloudflare).
A spoofed fingerprint on such an account reads as a new device and triggers verification, so a
real profile spoofs nothing. Create it with `--fingerprint real` (CLI), `fingerprint: "real"`
(MCP) or `fingerprint: { mode: 'real' }` (library). `browser open` then reports `engine: "real"`,
and so does `browser status`.

What a real launch does not do:

- no generated fingerprint, user agent or Client Hints override, and no stored GPU
- no injected script, no per-tab CDP session, no auto-attach: the launcher's only CDP connection
installs stored cookies, if any, and watches for the browser to exit
- no kernel flags and no anti-detect flags except `--disable-blink-features=AutomationControlled`,
which keeps `navigator.webdriver` false as in a Chrome opened by hand
- no flag that weakens Chrome's security: the sandbox, Safe Browsing, popup blocking, sync and
component updates stay on; only `--no-first-run` and `--no-default-browser-check` are added
- no timezone and no locale: Chrome keeps the host's clock and language unless the profile sets
`timezone` (sent as `TZ`) or `language` (sent as `--lang`, which Chrome on macOS ignores; change
the language in Chrome's settings instead, it is stored in the profile)

What it keeps: the profile's own user-data directory, the CDP endpoint, lock and reuse, `close`,
the profile's proxy (with the WebRTC policy that stops UDP from bypassing it) and `chromePath`.

What it does not protect: anything. The browser is exactly as identifiable as your own Chrome on
this machine, and two real profiles on one machine look like the same device. Do not use it for
accounts that must not be linked. A real profile refuses `engine: "kernel"` or `"inject"` with
`INVALID_CONFIG`; open it with `auto`. Switching a profile to real drops its stored spoof settings
except the language, and its stored timezone (often the `America/New_York` default) unless the same
update sets one. A browser already running keeps its old mode until it is closed.

## Kernel engine

The seed is a hash of the profile id, so the same profile is the same machine and a duplicate is
Expand Down
15 changes: 11 additions & 4 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ idOrName.
| `--id <value>` | Optional stable id: 1-64 chars of letters, digits, hyphen or underscore, e.g. `"acme-fb-01"`. Must not start with `"tmp-"` (reserved for temporary `browser.launch` sessions). Omit to get a random 16-hex-character id. Must not already exist. |
| `--proxy <value>` | Proxy as a URL with an explicit port: `"<scheme>://[user:pass@]host:port"`. Schemes: http, https, socks5 (socks and socks5h are accepted and normalised to socks5). Percent-encode reserved characters in credentials, e.g. `"http://bob:p%40ss@10.0.0.1:8080"`. Example: `"socks5://gate.provider.net:1080"`. |
| `--timezone <value>` | IANA timezone id used for the browser clock and Intl output, e.g. `"America/New_York"` or `"Asia/Ho_Chi_Minh"`. Match it to the proxy exit country or the profile becomes trivially detectable. |
| `--fingerprint <generated\|real>` | `generated` (default) spoofs a consistent fingerprint built from language and platform. `real` keeps Chrome's own identity: no user agent or client hints override, no injected scripts, no timezone or locale change unless `--timezone` or `--language` is set. Use `real` for your own accounts signed in by hand (Google, Firebase, Cloudflare), where a spoofed fingerprint triggers new-device checks. A real profile rejects `--platform` and opens only with `--engine auto`. See [anti-detect.md](./anti-detect.md#real-mode). |
| `--language <value>` | BCP 47 language tag for navigator.language and the Accept-Language header, e.g. `"en-US"`, `"vi-VN"`, `"de-DE"`. Default `"en-US"`. |
| `--platform <value>` | navigator.platform value to report. Use exactly one of `"Win32"`, `"MacIntel"` or `"Linux x86_64"`; it must agree with the user agent. |
| `--tags <value...>` | Free-form labels for filtering, e.g. `["facebook", "client-acme"]`. Pass an array of strings. Repeat the flag or pass a comma-separated list. |
Expand All @@ -95,8 +96,11 @@ idOrName.
```bash
browser-profiles create "Acme Main" --id acme-main \
--proxy socks5://gate.provider.net:1080 --tags facebook,acme
browser-profiles create "Google Main" --id google-main --fingerprint real
```

A real profile is stored with `"fingerprint": { "mode": "real" }` and no default timezone.

## profile update

Change fields of an existing profile. Omitted fields are left untouched.
Expand All @@ -107,6 +111,7 @@ Change fields of an existing profile. Omitted fields are left untouched.
| `--proxy <value>` | Same format as `profile create`. Pass null to remove the proxy; omit to keep the current one. |
| `--no-proxy` | Clear proxy (sends null). |
| `--timezone <value>` | As in `profile create`. |
| `--fingerprint <generated\|real>` | As in `profile create`. Switching to `real` drops the stored spoof settings except the language, and the stored timezone unless `--timezone` is passed in the same update; switching back to `generated` gets a new stored GPU. A browser already running keeps its mode until it is closed. |
| `--language <value>` | As in `profile create`. |
| `--platform <value>` | As in `profile create`. |
| `--tags <value...>` | As in `profile create`. |
Expand All @@ -115,6 +120,7 @@ Change fields of an existing profile. Omitted fields are left untouched.
```bash
browser-profiles update acme-main --timezone America/New_York
browser-profiles update acme-main --no-proxy
browser-profiles update google-main --fingerprint real
```

## profile delete
Expand Down Expand Up @@ -143,10 +149,10 @@ profile returns the running browser with `reused=true`. Close it with `browser c
| `--headless` | Run Chrome without a visible window. Default false (a real window is far less detectable). Set true only on machines with no display. |
| `--start-url <value>` | Absolute URL to open in the first tab, e.g. `"https://example.com"`. Omit for a blank tab. |
| `--detached` | Let Chrome outlive the process that opened it. Default false: the opener keeps the browser protected (per-tab fingerprint injection, authenticated proxy relay) and closes it on exit. Set true only for scripts that must exit immediately; then only flag-level protections remain and an authenticated proxy will not work. Kernel mode is the exception: its spoof is in the Chrome flags, so later tabs stay spoofed after the opener exits. |
| `--engine <auto\|kernel\|inject>` | Where the fingerprint is applied. `kernel` needs a fingerprint-chromium binary and spoofs inside Chromium with no JavaScript hooks. `inject` patches stock Chrome over CDP. `auto` (default) uses kernel when the executable contains `--fingerprint-platform`, otherwise inject. |
| `--engine <auto\|kernel\|inject>` | Where the fingerprint is applied. `kernel` needs a fingerprint-chromium binary and spoofs inside Chromium with no JavaScript hooks. `inject` patches stock Chrome over CDP. `auto` (default) uses kernel when the executable contains `--fingerprint-platform`, otherwise inject. A real profile only accepts `auto`; `kernel` or `inject` fails with `INVALID_CONFIG`. |

Output: `{ profileId, wsEndpoint, port, pid, reused, detached, engine }`. `engine` is `kernel` or
`inject`.
Output: `{ profileId, wsEndpoint, port, pid, reused, detached, engine }`. `engine` is `kernel`,
`inject`, or `real` for a real profile (Chrome's own fingerprint, nothing applied).

## browser launch

Expand Down Expand Up @@ -185,7 +191,8 @@ Output: `{ closed: string[] }`.
List browsers that are currently running for this storage path, with their CDP endpoints. Lock
files left behind by crashed browsers are removed while scanning. No flags beyond the global ones.

Output: `{ running: [{ profileId, pid, port, wsEndpoint, startedAt, temporary }] }`.
Output: `{ running: [{ profileId, pid, port, wsEndpoint, startedAt, temporary, engine }] }`. `engine`
is what the browser launched with: `kernel`, `inject` or `real`.

## storage path

Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ credentials are never logged.
`config.json` is written atomically (temp file plus rename), so a reader never sees half a file.

`.browser-lock.json` is the cross-process record of a running Chrome: `pid`, `port`, `wsEndpoint`,
`startedAt`, and optionally the local relay `proxyUrl`, `detached`, and `claiming` while a launch is
in flight. It is what lets `browser close` and `browser status` work from a different process, and
`startedAt`, and optionally the `engine` it launched with (`kernel`, `inject` or `real`), the local
relay `proxyUrl`, `detached`, and `claiming` while a launch is in flight. It is what lets `browser close` and `browser status` work from a different process, and
what gets cleaned up when a browser crashed. See [browser-lifecycle.md](./browser-lifecycle.md).

## See also
Expand Down
17 changes: 14 additions & 3 deletions docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ const { wsEndpoint, close } = await profiles.launch(profile.id);
await close();
```

For your own accounts signed in by hand, create the profile real: it launches with Chrome's own
identity, no generated fingerprint, no override, no injected script, and no default timezone
([anti-detect.md](./anti-detect.md#real-mode)).

```typescript
const google = await profiles.create({ id: 'google-main', name: 'Google Main', fingerprint: { mode: 'real' } });
const { engine } = await profiles.launch(google.id); // 'real'; engine: 'kernel' | 'inject' throws
```

| Method | Description |
|--------|-------------|
| `create(config)` | Create a profile from a `ProfileConfig`. |
Expand All @@ -80,8 +89,9 @@ await close();
| `deleteGroup(groupId)` | Delete a group. |
| `moveToGroup(profileId, groupId)` | Move a profile into a group, or out with `null`. |

`LaunchResult` carries `wsEndpoint`, `pid`, `port`, `profileId`, `close()` and, when an already
running browser was returned, `reused: true`.
`LaunchResult` carries `wsEndpoint`, `pid`, `port`, `profileId`, `close()`, `engine` (`kernel`,
`inject`, or `real` for a real profile) and, when an already running browser was returned,
`reused: true`.

## Option types

Expand All @@ -92,7 +102,7 @@ interface ProfileConfig {
proxy?: ProxyConfig;
timezone?: string; // e.g. "America/New_York"
cookies?: ProfileCookie[];
fingerprint?: FingerprintConfig;
fingerprint?: FingerprintConfig | RealFingerprint; // { mode: 'real', language? }: Chrome's own identity
startUrls?: string[];
tags?: string[];
groupId?: string;
Expand All @@ -115,6 +125,7 @@ interface LaunchOptions {
slowMo?: number;
timeout?: number;
detached?: boolean; // let Chrome outlive this process (reduced protection)
engine?: 'auto' | 'kernel' | 'inject'; // default auto; a real profile accepts only auto
}
```

Expand Down
20 changes: 12 additions & 8 deletions docs/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,27 +96,31 @@ Tool names are the command names with `_` instead of the dot: `profile.list` bec
|------|---------|-----------|
| `profile_list` | List stored browser profiles, optionally filtered by group or tag. | `groupId?`, `tag?` |
| `profile_get` | Show one profile by id or name, including its proxy and fingerprint. | `idOrName` |
| `profile_create` | Create a new browser profile with its own storage, proxy and fingerprint. | `name`, `id?`, `proxy?`, `timezone?`, `language?`, `platform?`, `tags?`, `notes?` |
| `profile_update` | Change fields of an existing profile. Omitted fields are left untouched. | `idOrName`, `name?`, `proxy?` (null removes it), `timezone?`, `language?`, `platform?`, `tags?`, `notes?` |
| `profile_create` | Create a new browser profile with its own storage, proxy and fingerprint. | `name`, `id?`, `proxy?`, `timezone?`, `fingerprint?` (`generated` or `real`), `language?`, `platform?`, `tags?`, `notes?` |
| `profile_update` | Change fields of an existing profile. Omitted fields are left untouched. | `idOrName`, `name?`, `proxy?` (null removes it), `timezone?`, `fingerprint?`, `language?`, `platform?`, `tags?`, `notes?` |
| `profile_delete` | Delete a profile and all of its browser data. Irreversible. | `idOrName`, `force?` |
| `profile_duplicate` | Copy a profile settings (proxy, timezone, fingerprint) into a new profile with a new id. | `idOrName`, `name?` |
| `browser_open` | Open Chrome with a stored profile and return its CDP wsEndpoint. | `idOrName`, `headless?`, `startUrl?`, `detached?` |
| `browser_open` | Open Chrome with a stored profile and return its CDP wsEndpoint. | `idOrName`, `headless?`, `startUrl?`, `detached?`, `engine?` |
| `browser_launch` | Launch a throwaway Chrome with a random fingerprint and no saved profile. | `proxy?`, `headless?`, `randomFingerprint?` (default true), `detached?` |
| `browser_close` | Close the browser running for a profile, from any process. | `idOrName` |
| `browser_close_all` | Close every browser started from this storage path, including temporary sessions. | none |
| `browser_status` | List browsers currently running for this storage path, with their CDP endpoints. | none |
| `storage_path` | Print the directory where profiles and browser data are stored. | none |

Argument formats (id/name rules, proxy URLs, timezone, language, platform) are the flag
descriptions in [cli.md](./cli.md); the MCP input schemas carry the same text.
Argument formats (id/name rules, proxy URLs, timezone, fingerprint, language, platform) are the
flag descriptions in [cli.md](./cli.md); the MCP input schemas carry the same text.

`fingerprint: "real"` is for your own accounts signed in by hand: the browser keeps Chrome's own
identity and `browser_open` reports `engine: "real"`, so an agent can check that nothing was
spoofed before it touches the account. See [anti-detect.md](./anti-detect.md#real-mode).

Outputs:

- `browser_open` → `{ profileId, wsEndpoint, port, pid, reused, detached }`
- `browser_launch` → `{ profileId, wsEndpoint, port, pid, temporary: true, detached }`
- `browser_open` → `{ profileId, wsEndpoint, port, pid, reused, detached, engine }` (`kernel`, `inject` or `real`)
- `browser_launch` → `{ profileId, wsEndpoint, port, pid, temporary: true, detached, engine }`
- `browser_close` → `{ profileId, closed }`
- `browser_close_all` → `{ closed: string[] }`
- `browser_status` → `{ running: [{ profileId, pid, port, wsEndpoint, startedAt, temporary }] }`
- `browser_status` → `{ running: [{ profileId, pid, port, wsEndpoint, startedAt, temporary, engine }] }`
- `storage_path` → `{ path }`
- `profile_delete` → `{ id, deleted: true }`
- the `profile_*` tools otherwise return the stored profile object
Expand Down
Loading
Loading