diff --git a/shell/settings.html b/shell/settings.html
index ef05f08..e5fc06b 100644
--- a/shell/settings.html
+++ b/shell/settings.html
@@ -819,6 +819,14 @@
🛡️ Stealth
+
+
+
+
Stealth skip hosts
+
Sites that actively detect stealth patches and break when injected.
These sites get no stealth injection — they run as a normal browser.
+
+
+
@@ -1132,6 +1140,11 @@ Are you sure?
toggleField('field-customUserAgent', st.userAgent === 'custom');
toggleField('field-customAcceptLanguage', st.acceptLanguage === 'custom');
+ const skipHotsEl = document.getElementById('cfg-skipHosts');
+ if (skipHotsEl && st.skipHosts) {
+ skipHotsEl.value = st.skipHosts.join('\n');
+ }
+
// Sync
const sy = config.sync || {};
setChecked('cfg-chromeBookmarks', !!sy.chromeBookmarks);
@@ -1368,6 +1381,16 @@ Are you sure?
wire('cfg-chromeBookmarks', 'sync.chromeBookmarks');
wire('cfg-chromeProfile', 'sync.chromeProfile');
wire('cfg-trackingEnabled', 'behavior.trackingEnabled');
+
+ // Stealth skip hosts
+ const skipHotsEl = document.getElementById('cfg-skipHosts');
+ if (skipHotsEl) {
+ skipHotsEl.addEventListener('change', () => {
+ const hosts = skipHotsEl.value.split('\n').map(s => s.trim()).filter(Boolean);
+ saveConfig({stealth: { skipHosts: hosts }});
+ });
+ }
+
// Folder picker for screenshot storage path
document.getElementById('btn-chooseFolder').addEventListener('click', async () => {
try {
diff --git a/src/config/manager.ts b/src/config/manager.ts
index 0df8ed7..7484bf6 100644
--- a/src/config/manager.ts
+++ b/src/config/manager.ts
@@ -56,6 +56,7 @@ export interface TandemConfig {
stealthLevel: 'low' | 'medium' | 'high';
acceptLanguage: 'auto' | 'custom';
customAcceptLanguage: string;
+ skipHosts: string[];
};
// Sync (Chrome bookmarks import)
@@ -152,6 +153,7 @@ const DEFAULT_CONFIG: TandemConfig = {
stealthLevel: 'medium',
acceptLanguage: 'auto',
customAcceptLanguage: '',
+ skipHosts: ['x.com', 'twitter.com', 'abs.twimg.com'],
},
sync: {
chromeBookmarks: false,
diff --git a/src/config/tests/config.test.ts b/src/config/tests/config.test.ts
index f2a4736..d8f45e9 100644
--- a/src/config/tests/config.test.ts
+++ b/src/config/tests/config.test.ts
@@ -81,6 +81,7 @@ describe('ConfigManager', () => {
expect(config.stealth.stealthLevel).toBe('medium');
expect(config.stealth.acceptLanguage).toBe('auto');
expect(config.stealth.customAcceptLanguage).toBe('');
+ expect(config.stealth.skipHosts).toEqual(['x.com', 'twitter.com', 'abs.twimg.com']);
});
it('loads with correct autonomy defaults', () => {
diff --git a/src/main.ts b/src/main.ts
index 4af5207..13a0e4f 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -269,7 +269,7 @@ async function createWindow(): Promise {
contents.on('dom-ready', () => {
// Skip stealth injection on sites that detect and block stealth patches
const url = contents.getURL();
- if (isGoogleAuthUrl(url) || shouldSkipStealth(url)) {
+ if (isGoogleAuthUrl(url) || shouldSkipStealth(url, runtime?.configManager.getConfig().stealth.skipHosts)) {
log.info('🔑 Skipping stealth for:', url.substring(0, 60));
return;
}
diff --git a/src/utils/security.ts b/src/utils/security.ts
index 9022718..00dc2fa 100644
--- a/src/utils/security.ts
+++ b/src/utils/security.ts
@@ -76,23 +76,11 @@ export function isGoogleAuthUrl(rawValue: string): boolean {
);
}
-/**
- * Sites that actively detect stealth patches and break when injected.
- * These sites get no stealth injection — they run as a normal browser.
- */
-const STEALTH_SKIP_HOSTS = new Set([
- 'x.com',
- 'twitter.com',
- 'abs.twimg.com',
- 'zhipin.com',
- 'login.zhipin.com',
-]);
-
-export function shouldSkipStealth(rawValue: string): boolean {
+export function shouldSkipStealth(rawValue: string, skipHosts: Array = []): boolean {
const parsed = tryParseUrl(rawValue);
if (!parsed || !urlHasProtocol(parsed, 'http:', 'https:')) return false;
const host = parsed.hostname.replace(/^www\./, '');
- return STEALTH_SKIP_HOSTS.has(host);
+ return new Set(skipHosts).has(host);
}
export function isSearchEngineResultsUrl(rawValue: string): boolean {