Add Firefox cookie import and stealth mode

- Firefox cookie importer: reads cookies.sqlite with WAL-safe copy,
  profile detection via profiles.ini, cross-platform paths, domain filtering
- Stealth mode: opt-in via launch(stealth: true), patches navigator.webdriver,
  plugins/mimeTypes, permissions API, WebGL renderer, iframe isolation,
  languages, plus realistic Safari UA and context hardening
- Import tool now accepts 'safari' | 'firefox' source
- STEALTH.md reference documentation
- Upgraded @types/node to v25 for node:sqlite support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 23:03:15 +02:00
parent 8ca72632b2
commit 1d3192cffd
19 changed files with 1189 additions and 67 deletions
+7
View File
@@ -13,6 +13,13 @@ export declare class ClaudeBrowser {
private interceptPatterns;
constructor(options?: BrowserOptions);
launch(): Promise<void>;
/**
* Apply stealth patches via addInitScript.
* These run before any page script in all Playwright engines (WebKit included).
* Scripts are passed as strings since they execute in browser context, not Node.
* See STEALTH.md for full documentation.
*/
private applyStealthPatches;
private enterFullscreen;
private previewAction;
private setupErrorListener;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
+121 -2
View File
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
import { promisify } from 'node:util';
import { webkit } from 'playwright';
const execAsync = promisify(exec);
import * as firefox from './firefox.js';
import * as image from './image.js';
import * as safari from './safari.js';
export class ClaudeBrowser {
@@ -24,16 +25,32 @@ export class ClaudeBrowser {
fullscreen: options.fullscreen ?? false,
preview: options.preview ?? false,
previewDelay: options.previewDelay ?? 2000,
stealth: options.stealth ?? false,
};
}
async launch() {
this.browser = await webkit.launch({ headless: this.options.headless });
this.context = await this.browser.newContext({
const contextOptions = {
viewport: {
width: this.options.width,
height: this.options.height,
},
});
};
if (this.options.stealth) {
Object.assign(contextOptions, {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15',
locale: 'en-US',
timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone,
colorScheme: 'light',
extraHTTPHeaders: {
'Accept-Language': 'en-US,en;q=0.9',
},
});
}
this.context = await this.browser.newContext(contextOptions);
if (this.options.stealth) {
await this.applyStealthPatches();
}
this.page = await this.context.newPage();
this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page);
@@ -43,6 +60,85 @@ export class ClaudeBrowser {
await this.enterFullscreen();
}
}
/**
* Apply stealth patches via addInitScript.
* These run before any page script in all Playwright engines (WebKit included).
* Scripts are passed as strings since they execute in browser context, not Node.
* See STEALTH.md for full documentation.
*/
async applyStealthPatches() {
if (!this.context)
return;
// 1. WebDriver flag — set to undefined, not false
// Some detectors specifically check for false as a signal of patching
await this.context.addInitScript(`
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
`);
// 2. Plugins & MimeTypes — headless reports empty arrays
await this.context.addInitScript(`
Object.defineProperty(navigator, 'plugins', {
get: () => [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
],
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => [
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
],
});
`);
// 3. Permissions API — fix notifications query inconsistency
await this.context.addInitScript(`
const __origQuery = navigator.permissions.query.bind(navigator.permissions);
Object.defineProperty(navigator.permissions, 'query', {
value: (params) =>
params.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: __origQuery(params),
});
`);
// 4. WebGL renderer masking — spoof GPU vendor/renderer
// Params 37445 (UNMASKED_VENDOR_WEBGL) and 37446 (UNMASKED_RENDERER_WEBGL)
await this.context.addInitScript(`
const __origGetParam = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(p) {
if (p === 37445) return 'Apple GPU';
if (p === 37446) return 'Apple M1 Pro';
return __origGetParam.call(this, p);
};
if (typeof WebGL2RenderingContext !== 'undefined') {
const __origGetParam2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(p) {
if (p === 37445) return 'Apple GPU';
if (p === 37446) return 'Apple M1 Pro';
return __origGetParam2.call(this, p);
};
}
`);
// 5. iframe contentWindow isolation — apply webdriver patch in child frames
await this.context.addInitScript(`
const __iframeDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
if (__iframeDesc) {
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
get: function() {
const win = __iframeDesc.get?.call(this);
if (win) {
try {
Object.defineProperty(win.navigator, 'webdriver', { get: () => undefined });
} catch(e) {}
}
return win;
},
});
}
`);
// 6. Languages consistency
await this.context.addInitScript(`
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
`);
}
async enterFullscreen() {
if (process.platform !== 'darwin') {
console.warn('Native fullscreen only supported on macOS');
@@ -699,6 +795,29 @@ export class ClaudeBrowser {
domains,
};
}
if (cmd.source === 'firefox') {
const cookies = firefox.importFirefoxCookies({
domain: cmd.domain,
profile: cmd.profile,
});
if (cookies.length === 0) {
return {
ok: true,
imported: 0,
source: 'firefox',
domains: [],
};
}
const playwrightCookies = cookies.map(firefox.toPlaywrightCookie);
await context.addCookies(playwrightCookies);
const domains = [...new Set(cookies.map((c) => c.domain))];
return {
ok: true,
imported: cookies.length,
source: 'firefox',
domains,
};
}
return { ok: false, error: `Unknown import source: ${cmd.source}` };
}
async executeCommand(cmd) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+58
View File
@@ -0,0 +1,58 @@
/**
* Firefox cookie importer
*
* Reads cookies from Firefox's cookies.sqlite database.
* Firefox stores cookies as plain unencrypted SQLite — no binary parsing needed.
*
* Database schema (moz_cookies table):
* id, originAttributes, name, value, host, path, expiry,
* lastAccessed, creationTime, isSecure, isHttpOnly,
* inBrowserElement, sameSite, rawSameSite, schemeMap
*
* Note: expiry is Unix seconds, but lastAccessed/creationTime are microseconds.
*
* Firefox holds an exclusive WAL lock while running, so we copy the database
* files (cookies.sqlite + WAL + SHM) to a temp directory before reading.
*/
export interface FirefoxCookie {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'None' | 'Lax' | 'Strict';
}
interface FirefoxProfile {
name: string;
path: string;
isRelative: boolean;
isDefault: boolean;
}
/**
* List available Firefox profiles
*/
export declare function listFirefoxProfiles(): FirefoxProfile[];
/**
* Import cookies from Firefox's cookies.sqlite database
*/
export declare function importFirefoxCookies(options?: {
profile?: string;
domain?: string;
}): FirefoxCookie[];
/**
* Convert FirefoxCookie to Playwright cookie format
*/
export declare function toPlaywrightCookie(cookie: FirefoxCookie): {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
};
export {};
//# sourceMappingURL=firefox.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"firefox.d.ts","sourceRoot":"","sources":["../src/firefox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;CACrC;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAuFD;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,EAAE,CAItD;AAsFD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,EAAE,CAmClB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CACrC,CAWA"}
+229
View File
@@ -0,0 +1,229 @@
/**
* Firefox cookie importer
*
* Reads cookies from Firefox's cookies.sqlite database.
* Firefox stores cookies as plain unencrypted SQLite — no binary parsing needed.
*
* Database schema (moz_cookies table):
* id, originAttributes, name, value, host, path, expiry,
* lastAccessed, creationTime, isSecure, isHttpOnly,
* inBrowserElement, sameSite, rawSameSite, schemeMap
*
* Note: expiry is Unix seconds, but lastAccessed/creationTime are microseconds.
*
* Firefox holds an exclusive WAL lock while running, so we copy the database
* files (cookies.sqlite + WAL + SHM) to a temp directory before reading.
*/
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { homedir, platform, tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
/**
* Finalize a partial profile into a full FirefoxProfile if valid
*/
function finalizeProfile(partial) {
if (!partial?.name || !partial?.path)
return null;
return {
name: partial.name,
path: partial.path,
isRelative: partial.isRelative ?? true,
isDefault: partial.isDefault ?? false,
};
}
/**
* Apply a key=value line to a partial profile
*/
function applyProfileField(profile, key, value) {
if (key === 'Name')
profile.name = value;
else if (key === 'Path')
profile.path = value;
else if (key === 'IsRelative')
profile.isRelative = value === '1';
else if (key === 'Default')
profile.isDefault = value === '1';
}
/**
* Process a single line of profiles.ini, updating state
*/
function processIniLine(line, current, profiles) {
const trimmed = line.trim();
if (trimmed.startsWith('[Profile') || trimmed.startsWith('[Install')) {
const finalized = finalizeProfile(current);
if (finalized)
profiles.push(finalized);
return trimmed.startsWith('[Profile') ? {} : null;
}
if (current) {
const eqIdx = trimmed.indexOf('=');
if (eqIdx !== -1)
applyProfileField(current, trimmed.slice(0, eqIdx), trimmed.slice(eqIdx + 1));
}
return current;
}
/**
* Parse Firefox profiles.ini to find available profiles
*/
function parseProfilesIni(iniPath) {
if (!existsSync(iniPath))
return [];
const lines = readFileSync(iniPath, 'utf-8').split('\n');
const profiles = [];
let current = null;
for (const line of lines) {
current = processIniLine(line, current, profiles);
}
const last = finalizeProfile(current);
if (last)
profiles.push(last);
return profiles;
}
/**
* Get the Firefox profiles root directory for the current platform
*/
function getFirefoxRoot() {
const home = homedir();
switch (platform()) {
case 'darwin':
return join(home, 'Library/Application Support/Firefox');
case 'linux':
return join(home, '.mozilla/firefox');
case 'win32':
return join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Mozilla/Firefox');
default:
throw new Error(`Unsupported platform: ${platform()}`);
}
}
/**
* List available Firefox profiles
*/
export function listFirefoxProfiles() {
const root = getFirefoxRoot();
const iniPath = join(root, 'profiles.ini');
return parseProfilesIni(iniPath);
}
/**
* Resolve a FirefoxProfile to its absolute path
*/
function profileToAbsolutePath(root, p) {
return p.isRelative ? join(root, p.path) : p.path;
}
/**
* Resolve the full path to a Firefox profile directory
*/
function resolveProfilePath(profile) {
const root = getFirefoxRoot();
const profiles = listFirefoxProfiles();
if (!profile) {
const defaultProfile = profiles.find((p) => p.isDefault) || profiles[0];
if (!defaultProfile)
throw new Error('No Firefox profiles found. Is Firefox installed?');
return profileToAbsolutePath(root, defaultProfile);
}
// Try exact match by name or path
const match = profiles.find((p) => p.name === profile || p.path === profile);
if (match)
return profileToAbsolutePath(root, match);
// Try as direct path fragment in Profiles dir
const directPath = join(root, 'Profiles', profile);
if (existsSync(directPath))
return directPath;
// Try as absolute path
if (existsSync(profile))
return profile;
const available = profiles.map((p) => p.name).join(', ');
throw new Error(`Firefox profile not found: "${profile}". Available: ${available}`);
}
/**
* Safely copy the Firefox cookies database to a temp directory.
* Copies cookies.sqlite + WAL + SHM files to avoid lock conflicts.
*/
function copyDatabaseSafely(dbPath) {
if (!existsSync(dbPath)) {
throw new Error(`Firefox cookies database not found at: ${dbPath}\nMake sure Firefox has been used at least once.`);
}
const tmpDir = mkdtempSync(join(tmpdir(), 'browse-fx-'));
const dbName = 'cookies.sqlite';
try {
// Copy main database
copyFileSync(dbPath, join(tmpDir, dbName));
// Copy WAL and SHM if they exist (needed for up-to-date reads)
for (const ext of ['-wal', '-shm']) {
const src = `${dbPath}${ext}`;
if (existsSync(src)) {
copyFileSync(src, join(tmpDir, `${dbName}${ext}`));
}
}
return { tmpDir, tmpDbPath: join(tmpDir, dbName) };
}
catch (err) {
// Clean up on failure
rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
}
/**
* Convert Firefox sameSite integer to string
* 0 = None, 1 = Lax, 2 = Strict
*/
function sameSiteToString(value) {
switch (value) {
case 2:
return 'Strict';
case 1:
return 'Lax';
default:
return 'None';
}
}
/**
* Import cookies from Firefox's cookies.sqlite database
*/
export function importFirefoxCookies(options) {
const profilePath = resolveProfilePath(options?.profile);
const dbPath = join(profilePath, 'cookies.sqlite');
const { tmpDir, tmpDbPath } = copyDatabaseSafely(dbPath);
try {
const db = new DatabaseSync(tmpDbPath, { readOnly: true });
let query = 'SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite FROM moz_cookies';
const params = [];
if (options?.domain) {
const domain = options.domain.toLowerCase();
query += ' WHERE LOWER(host) = ? OR LOWER(host) = ? OR LOWER(host) LIKE ?';
params.push(domain, `.${domain}`, `%.${domain}`);
}
const stmt = db.prepare(query);
const rows = params.length > 0 ? stmt.all(...params) : stmt.all();
db.close();
return rows.map((row) => ({
name: row.name,
value: row.value,
domain: row.host,
path: row.path,
expires: row.expiry,
secure: row.isSecure === 1,
httpOnly: row.isHttpOnly === 1,
sameSite: sameSiteToString(row.sameSite),
}));
}
finally {
rmSync(tmpDir, { recursive: true, force: true });
}
}
/**
* Convert FirefoxCookie to Playwright cookie format
*/
export function toPlaywrightCookie(cookie) {
return {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
expires: cookie.expires,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
sameSite: cookie.sameSite,
};
}
//# sourceMappingURL=firefox.js.map
+1
View File
File diff suppressed because one or more lines are too long
Vendored
+16 -5
View File
@@ -18,6 +18,7 @@ let browserOptions = {
fullscreen: false,
preview: false,
previewDelay: 2000,
stealth: false,
};
let browser = new ClaudeBrowser(browserOptions);
let launched = false;
@@ -72,7 +73,12 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
previewDelay: z.number().optional().default(2000).describe('Preview highlight duration in ms'),
width: z.number().optional().default(1280).describe('Viewport width'),
height: z.number().optional().default(800).describe('Viewport height'),
}, withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height }) => {
stealth: z
.boolean()
.optional()
.default(false)
.describe('Enable stealth mode to reduce bot detection. Patches navigator.webdriver, plugins, WebGL, permissions, and sets a realistic Safari user-agent. See STEALTH.md for details.'),
}, withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height, stealth }) => {
// Close existing browser if launched
if (launched) {
await browser.close();
@@ -86,6 +92,7 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
fullscreen,
preview,
previewDelay,
stealth,
};
// Create new browser with updated options
browser = new ClaudeBrowser(browserOptions);
@@ -93,12 +100,13 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
launched = true;
return textResult(JSON.stringify({
ok: true,
message: 'Browser launched',
message: `Browser launched${stealth ? ' (stealth mode)' : ''}`,
options: {
headed: !browserOptions.headless,
fullscreen,
preview,
previewDelay,
stealth,
viewport: { width, height },
},
}));
@@ -447,13 +455,16 @@ server.tool('session_restore', 'Restore a previously saved session state from a
}));
}));
// Browser import
server.tool('import', 'Import cookies from Safari browser (macOS only). Requires Full Disk Access permission.', {
source: z.enum(['safari']).describe('Browser to import from (currently only Safari supported)'),
server.tool('import', 'Import cookies from Safari or Firefox browser. Safari requires Full Disk Access permission (macOS only). Firefox works on macOS, Linux, and Windows.', {
source: z.enum(['safari', 'firefox']).describe('Browser to import from'),
domain: z
.string()
.optional()
.describe('Filter cookies to specific domain (e.g., "github.com")'),
profile: z.string().optional().describe('Safari profile/WebKit data store ID (optional)'),
profile: z
.string()
.optional()
.describe('Safari profile/WebKit data store ID, or Firefox profile name (optional)'),
}, withLogging('import', async ({ source, domain, profile }) => {
await ensureLaunched();
const result = await browser.executeCommand({ cmd: 'import', source, domain, profile });
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -5,6 +5,7 @@ export interface BrowserOptions {
fullscreen?: boolean;
preview?: boolean;
previewDelay?: number;
stealth?: boolean;
}
export interface ElementInfo {
tag: string;
@@ -254,7 +255,7 @@ export interface EmulateCommand {
}
export interface ImportCommand {
cmd: 'import';
source: 'safari';
source: 'safari' | 'firefox';
domain?: string;
profile?: string;
}
+1 -1
View File
File diff suppressed because one or more lines are too long