DUPPEL · RELEASE COMPENDIUM · v0.1.1

An anti-fingerprinting
browser extension,
built by an AI mesh.

Tracking-pollution and fingerprint-resilience for Chrome MV3. From first concept on May 9 to v0.1.1 release on May 24 — fifteen days, built by three frontier models from three different labs.

Version
v0.1.1

Released:
2026-05-24

Commit
73bde1f

Manifest
MV3

Tests
281 (3 skip)

Mission
Pollution

Duppel is a Chrome MV3 extension for tracking-data pollution and fingerprint resilience. The name comes from the German WWII term Düppel —radar chaff, the foil strips bombers used to confuse enemy radar. The metaphor fits. Duppel does not try to make you invisible to trackers. It makes the data they collect on you unreliable and unbelievable, so the identity graphs they build are worth less and link less confidently across sessions.

This page is the canonical compendium for the v0.1.1 release. It includes the technical writeup, the formal specification, the measurement reports from Phases B and C, the controlled smoke matrix, and the release record. Read them in order, or jump to what interests you.

Built by AgentMesh: Snapdragon (Anthropic Claude Code) on implementation,
Rowan (OpenAI GPT-5.5) Q (Qwen3.6-35B-A3B) on engineering review, Lux (Google Gemini) on methodology,
Atlas (Anthropic Claude Code) as orchestrator, and a human gatekeeper for production decisions.
All personal infrastructure, all personal time.

Contents

README.MD
README
Project entry point

Duppel

Duppel is the AgentMesh privacy chassis for tracking-pollution and fingerprint-resilience work. The former project name is retired; current documentation and release references should use Duppel.


Current Release

  • Version: v0.1.1
  • Commit: 73bde1f3f95f8e4a19210aff772e3c05415f8695
  • Artifact: /Volumes/Mesh/Duppel/duppel-v0.1.1.zip
  • SHA256: 05d884ccff2011e217df07beaaff5eb5e108abf35ea3f0e978c4823d78085ab5
  • GitHub release: https://github.com/alex-hinojosa/duppel/releases/tag/v0.1.1
  • Smoke evidence: /Volumes/Mesh/Duppel/v0.1.1-controlled-smoke/

v0.1.1 Summary

v0.1.1 ships strict-next-nav coherence. First document navigation stays native; persona mode starts on the next navigation. This prevents first-contact split-brain between native browser/TLS signals and extension-controlled JavaScript or HTTP identity surfaces.


Known Limitation

Duppel is not a bot-detection bypass product. Some high-posture Cloudflare Enterprise sites may challenge persona mode. Native-Compatible Mode restores native browser identity behavior for those sites and was verified during the v0.1.1 controlled smoke matrix for OpenAI and B&H Photo.


Evidence

  • Release specification: /Volumes/Mesh/Duppel/SPEC-v0.1.1.md
  • Package smoke summary: /Volumes/Mesh/Duppel/v0.1.1-controlled-smoke/SMOKE-SUMMARY.md
  • Phase B report: /Volumes/Mesh/Duppel/PHASE-B-REPORT.md
  • Phase C report: /Volumes/Mesh/Duppel/PHASE-C-REPORT.md
  • Phase D report: /Volumes/Mesh/Duppel/phase-d/PHASE-D-REPORT.md


duppel.md

Duppel · Technical Writeup

Full architecture, defense surfaces, debug story, and mesh attribution


Duppel: Anti-Fingerprint Identity Spoofing for Chrome

A Chrome MV3 extension that defeats browser fingerprinting through deterministic identity spoofing, tracker data poisoning, and ultrasonic cross-device tracking defense.

Built by a six-agent AI mesh (atlas, lux, rowan, snapdragon, + project agents) with human oversight. The architecture, code review, and bug resolution described below were performed entirely by AI agents collaborating through an asynchronous message-based coordination system.

Last updated: 2026-05-24. Current release: v0.1.1 (commit 73bde1f on main).



The Problem

Browser fingerprinting has replaced cookies as the primary cross-site tracking mechanism. Unlike cookies, fingerprints can’t be cleared. They’re assembled from dozens of browser API surfaces — your GPU model, screen resolution, installed fonts, audio processing characteristics, timezone, hardware specs, and more — to create a unique identifier that persists across sessions, private browsing, and VPN use.

The industry standard test site amiunique.org reports that 89.4% of browsers have a unique fingerprint. In practice, combining just 3-4 high-entropy surfaces (canvas hash + WebGL renderer + screen resolution + timezone) is sufficient to uniquely identify most users.

Duppel intercepts these API surfaces before any page script runs and returns plausible spoofed values derived from a deterministic seed. The result: every session presents a coherent but fake identity to fingerprinting services.



Architecture Overview

Duppel runs as a Chrome Manifest V3 extension with three execution contexts:

Component Execution World Role
anti-fingerprint.js MAIN (page context) Intercepts browser APIs before page scripts load
bridge.js ISOLATED (extension context) Monitors page lifecycle events for the service worker
background.js Service Worker Identity management, DNR proof gating, strict-next-nav, rotation, cookie cleanup, beacon generation

The MAIN world injection is critical. Content scripts normally run in an ISOLATED world that can’t modify the page’s JavaScript environment. Duppel injects a bootstrap function into the MAIN world via chrome.scripting.executeScript with closure-local arguments — the session seed and profile are passed directly as function parameters, never exposed as window properties or sessionStorage entries. When a fingerprinting script later calls navigator.userAgent or canvas.toDataURL(), it hits our spoofed implementation, not the real one.

Modular Source Structure

The main content script is bundled from modular source files via esbuild:

src/content/anti-fingerprint/
  index.js       Bundle entry point
  core.js        Shared utilities (disguise, spoof, GOPD normalization)
  canvas.js      Canvas 2D noise (toDataURL, toBlob, getImageData)
  webgl.js       WebGL spoofing (params, readPixels, OffscreenCanvas)
  navigator.js   Navigator property spoofing
  screen.js      Screen dimensions, DPR, matchMedia
  audio.js       AudioContext noise, ultrasonic attenuation
  biometric.js   Timing and input precision reduction
  misc.js        Timezone, enumerateDevices, Worker/SharedWorker wrapping
  iframe.js      Cross-frame consistency

Build: npm run build runs esbuild and produces anti-fingerprint.js. npm run build:chrome and npm run build:firefox produce complete builds in build/chrome/ and build/firefox/.

Closure-Local Bootstrap (v0.1.0+)

The original v1 architecture used sessionStorage as a bridge between the MAIN and ISOLATED worlds. This had a critical flaw: chrome.storage.session is not accessible from content scripts without explicit setAccessLevel configuration, and every storage call was silently failing (see Debug Story below).

v0.1.0 replaced the entire bridge mechanism with closure-local injection. The service worker listens for webNavigation.onCommitted, then calls chrome.scripting.executeScript({ func, args: [seed, profile] }). The bootstrap function receives the seed and profile as closure-local arguments — never exposed as window properties, sessionStorage entries, or any other observable surface. This eliminates the timing race entirely: the function either runs with the correct seed or doesn’t run at all.

Strict-Next-Nav Coherence (v0.1.1)

v0.1.1 introduces strict-next-nav (strictFirstDoc: true), the production default. The problem it solves: on first navigation to a site, executeScript races with the page’s JS context establishment. If the bootstrap arrives late, the page runs with native APIs while the HTTP User-Agent header is spoofed via DNR — a split-brain that fingerprinting services can detect.

Strict-next-nav makes the first document intentionally all-native:

  1. First navigation (onCommitted): Skip executeScript. Install a main_frame-only DNR arming rule (separate from the full session rule) that primes the next navigation’s HTTP UA.
  2. Second navigation: executeScript runs and the bootstrap function proves itself via .then(). On success, the arming rule is replaced with the full session rule covering all resource types.

The result: first contact with any site is always clean (native identity). Persona activates on the second navigation with full HTTP + JS coherence. No split-brain, no challenge loops.

Persona-Family Filtering (v0.1.1)

The profile pool is filtered by host OS and browser engine at startup. A Windows Chrome user only receives Windows Chrome personas. This prevents cross-family contradictions (e.g., a Linux persona on a Windows host) that are trivially detectable.

Native-Compatible Mode (v0.1.1)

Per-site toggle that suppresses all spoofing for a given eTLD+1. Designed for sites that break with fingerprint protection — Cloudflare Enterprise challenges, banking logins, payment processors. The toggle persists across restarts via chrome.storage.local. When enabled for a site, executeScript is skipped and no DNR rules are applied.

Why a Seeded PRNG?

Every spoofed value in a session derives from a single integer seed via Mulberry32 (a fast 32-bit PRNG). This ensures internal consistency — the GPU, screen, timezone, UA, and all other values come from the same deterministic derivation and form a plausible persona. It also ensures intra-session stability — calling navigator.userAgent 1,000 times returns the same value. Instability across repeated reads is itself a tampering detection signal that fingerprinting services check for.



Defense Surfaces

1. Navigator Properties

What’s spoofed: userAgent, platform, hardwareConcurrency, deviceMemory, languages, language, vendor, appVersion, webdriver, maxTouchPoints, connection, globalPrivacyControl

Why: These are the lowest-hanging fingerprinting fruit. navigator.userAgent alone carries browser name, version, OS, and architecture. hardwareConcurrency (CPU core count) and deviceMemory (RAM in GB) are high-entropy values that most users don’t realize are exposed to every website. webdriver is set to false to prevent bot detection flags. maxTouchPoints is zeroed to prevent leaking that you’re on a touchscreen device (like a Surface Pro). connection (Network Information API) is hidden entirely — it exposes your connection type (wifi/cellular/ethernet), effective bandwidth, and RTT, all of which are fingerprinting vectors. globalPrivacyControl is set to true, signaling the GPC opt-out preference (matched by a Sec-GPC: 1 HTTP header sent via declarativeNetRequest).

Method: Object.defineProperty on the prototype with getter functions. The getters return values derived from the session seed. All wrapper functions have their .toString() patched via a disguise() helper to return function userAgent() { [native code] } rather than revealing the override source code.

2. Client Hints (navigator.userAgentData)

What’s spoofed: brands, mobile, platform, getHighEntropyValues() (returns platformVersion, architecture, bitness, model, uaFullVersion, fullVersionList, wow64)

Why: Chrome’s User-Agent Client Hints (UA-CH) are the successor to the User-Agent string. They provide structured, high-entropy data about the browser and OS. getHighEntropyValues() is particularly dangerous — it returns architecture (x86 vs arm), exact platform version, and bitness, which together can narrow identification significantly. Many fingerprinting services now check UA-CH before falling back to the UA string.

Method: A complete userAgentData object is constructed with brands derived from the spoofed UA string (Chrome, Edge, or Firefox). The getHighEntropyValues() method returns a resolved Promise with consistent values. Architecture is inferred from the spoofed GPU (Apple Silicon GPUs return arm; everything else returns x86).

3. Screen and Viewport Dimensions

What’s spoofed: screen.width, screen.height, screen.availWidth, screen.availHeight, screen.colorDepth, screen.pixelDepth, window.innerWidth, window.innerHeight, window.outerWidth, window.outerHeight, window.devicePixelRatio, window.visualViewport.width/height/scale, screenX, screenY, availLeft, availTop

Why: Screen resolution is one of the highest-entropy fingerprinting surfaces. The combination of screen dimensions + DPR + available height (which varies by taskbar size and OS) creates a near-unique identifier. visualViewport is a newer API that some fingerprinting services cross-reference against innerWidth/innerHeight — any inconsistency between them is a spoofing detection signal. Screen position (screenX/screenY, availLeft/availTop) can leak multi-monitor configuration; these are zeroed.

Method: Dimensions are selected from a pool of 9 common resolutions (1920×1080 through 3840×2160). devicePixelRatio is set to 2 for 4K resolutions and 1 for everything else, matching real-world behavior. Browser chrome height (the gap between inner and outer height) is derived deterministically from the canvas seed to vary realistically between 80-120px. All viewport-related surfaces are patched consistently to prevent cross-surface comparison attacks.

4. CSS Media Queries (matchMedia)

What’s spoofed: Full CSS media query evaluation against spoofed values — dimensions (width, height, device-width, device-height), resolution/DPR (including -webkit-device-pixel-ratio), aspect-ratio, color/color-index/monochrome, orientation, pointer/hover capabilities

Why: window.matchMedia() is a subtle but powerful fingerprinting vector. A tracker can probe your real screen dimensions without ever reading screen.width by testing media queries like (min-width: 1920px) and (max-width: 1920px) in a binary search. This bypasses naive navigator/screen spoofing. The -webkit-device-pixel-ratio variant is particularly common in Chromium fingerprinting because it reveals the real DPR even when window.devicePixelRatio is spoofed.

Method: A complete media query parser that handles legacy syntax (min-width: 1024px), MQ Level 4 range syntax (width >= 1024px), reversed ranges (1024px <= width), double ranges (400px < width < 1200px), and all CSS length units (px, em, rem, vw, vh, cm, in, pt, pc, mm, vmin, vmax). Hardware-identifying features fail closed -- if a value uses an unparseable unit, it returns false rather than falling through to the real matchMedia (which would leak real dimensions). Preference features (prefers-color-scheme, prefers-reduced-motion) pass through to preserve dark mode and accessibility.

5. Canvas Fingerprinting

What's spoofed: HTMLCanvasElement.prototype.toDataURL, HTMLCanvasElement.prototype.toBlob, CanvasRenderingContext2D.prototype.getImageData, CanvasRenderingContext2D.prototype.measureText, OffscreenCanvas.prototype.convertToBlob, OffscreenCanvasRenderingContext2D.prototype.getImageData

Why: Canvas fingerprinting is the second most common fingerprinting technique after navigator properties. A tracker draws specific text, gradients, and shapes to a hidden canvas, then reads the pixel data. The rendered output differs based on GPU, driver version, font rendering engine, anti-aliasing implementation, and sub-pixel rendering -- creating a hash that's unique to your hardware/software combination. measureText is a related vector: the exact width returned for a given string varies by font rendering engine and installed fonts. OffscreenCanvas is the same attack surface available in both main-thread and Worker contexts.

Method: Deterministic per-pixel noise using hash(canvasSeed + pixelIndex + pixelValue). The noise is +/-1 per RGB channel -- enough to change the canvas hash (defeating exact-match fingerprinting) while being invisible to the human eye. The key design constraint is determinism: calling toDataURL() on the same canvas content must return the same result every time within a session. A non-deterministic approach (random noise) would be trivially detected by calling toDataURL() twice and comparing.

For toDataURL and toBlob, an offscreen clone canvas is created, the original content is drawn onto it, noise is applied, and the result is returned from the clone -- preventing visible corruption of canvases the user can see. measureText gets +/-0.1px deterministic noise derived from hash(canvasSeed + text + font).

OffscreenCanvas.convertToBlob uses the same clone pattern. OffscreenCanvasRenderingContext2D.getImageData is patched in both the main world and Worker scopes (see Section 11).

6. Audio Fingerprinting

What's spoofed: AudioBuffer.prototype.getChannelData

Why this matters -- and why almost nobody knows about it: Audio fingerprinting is one of the most insidious tracking techniques in production today, and it's virtually unknown outside of the fingerprinting research community.

Here's how it works: a tracker creates an OfflineAudioContext, generates a test signal (typically an oscillator through a compressor), renders it, and reads the resulting audio samples. The output is deterministic for a given browser+OS+hardware combination -- but varies across devices due to differences in floating-point implementation, audio processing pipeline, sample rate conversion, and DSP behavior. The resulting hash is high-entropy, invisible to the user (no audio is ever played), and resistant to most privacy tools.

FingerprintJS (used by ~15% of the top 10K websites) uses audio fingerprinting as one of its primary signals. The technique was first documented in the 2016 Princeton Web Transparency & Accountability Project paper "Online Tracking: A 1-Million-Site Measurement and Analysis" and has since become standard in commercial fingerprinting SDKs.

What makes audio fingerprinting particularly dangerous:

  • It's invisible. No microphone access is needed. No sound is played. No permission prompt is shown.
  • It's stable. The fingerprint doesn't change between sessions, private browsing modes, or VPN use.
  • It survives cookie clearing. It's purely computation-based.
  • It's hard to block. Disabling the Web Audio API breaks legitimate audio applications.
  • It's rarely discussed. Most privacy guides focus on cookies, IP addresses, and canvas -- audio fingerprinting flies under the radar.

Method: Deterministic micro-noise applied to getChannelData output. Each sample gets +/-(hash(audioSeed + channel + sampleIndex + quantizedValue)) * 0.0000005 -- small enough to be inaudible but large enough to change the audio fingerprint hash. A WeakMap tracks which buffer+channel combinations have been noised to prevent compounding noise on repeated reads (since getChannelData returns a live reference to the underlying Float32Array).

7. WebAudio Ultrasonic Cross-Device Tracking Defense

What it does: Inserts a BiquadFilter (highshelf at 17,999 Hz, -70 dB gain) into the audio graph before any AudioDestinationNode (speakers) or AnalyserNode (FFT analysis)

Why: This defends against an entirely different threat than audio fingerprinting. Ultrasonic cross-device tracking (UXDT) uses near-ultrasonic audio signals (18-20 kHz) embedded in TV commercials, web pages, or mobile apps to link devices belonging to the same person. Your phone's microphone picks up a TV commercial's ultrasonic beacon; a tracking SDK in an unrelated app hears it and reports back. Your laptop's speakers can also emit these beacons from a web page, linking your browsing session to your phone.

This technique was first documented by researchers studying the SilverPush SDK (found in 234 Android apps in 2015). The FTC issued warnings. Academic work at PETS 2017 (Mavroudis et al., "On the Privacy and Security of the Ultrasound Ecosystem") demonstrated the feasibility and proposed the Silverdog/SilverWall defense approach that Duppel implements.

Method: AudioNode.prototype.connect() is intercepted. When a node connects to an AudioDestinationNode (speakers) or AnalyserNode (frequency analysis), a BiquadFilter is transparently inserted in the audio chain. The filter applies -70 dB attenuation to everything above ~18 kHz -- effectively silencing ultrasonic content while leaving audible audio (<17 kHz) untouched. The AnalyserNode interception is critical: UXDT trackers often route MediaElementSource -> AnalyserNode to read ultrasonic frequencies via FFT analysis, upstream of any speaker output.

8. WebGL GPU Spoofing and Pixel Noise

What's spoofed: UNMASKED_VENDOR_WEBGL, UNMASKED_RENDERER_WEBGL (via getParameter), getSupportedExtensions(), WebGLRenderingContext.prototype.readPixels, WebGL2RenderingContext.prototype.readPixels

Why: The WebGL debug renderer info is one of the highest-entropy fingerprinting surfaces. Your exact GPU model string (e.g., "ANGLE (NVIDIA, NVIDIA GeForce RTX 4070, OpenGL 4.5)") combined with the supported WebGL extensions list can narrow identification to a very small group. The renderer string includes the GPU model, driver version format, and graphics API, all of which vary across hardware.

readPixels is the WebGL equivalent of canvas fingerprinting -- a tracker can render a scene to a WebGL context, read the pixel data, and hash it. GPU-specific rendering differences (rasterization rules, floating-point precision, driver-level optimizations) make the output hardware-unique.

Method: GPU vendor/renderer are replaced with values from a correlated profile group (see Profile Coherence below). The extensions list is normalized to a common baseline of 20 widely-supported extensions, removing hardware-specific extensions that would leak real GPU identity. readPixels is intercepted for both WebGLRenderingContext and WebGL2RenderingContext -- when the format is RGBA/UNSIGNED_BYTE, the same deterministic pixel noise from the canvas pipeline is applied to the output buffer.

9. Timezone Spoofing (DST-Aware)

What's spoofed: Date.prototype.getTimezoneOffset, Intl.DateTimeFormat.prototype.resolvedOptions, Intl.DateTimeFormat constructor

Why: Timezone is a medium-entropy fingerprinting surface. Combined with language and locale, it significantly narrows geographic location. The interesting challenge is DST (Daylight Saving Time) -- a naive implementation that returns a fixed offset will fail when a fingerprinting service checks the offset for January vs. July dates. A timezone that claims to be America/New_York but returns the same offset year-round is obviously spoofed.

Method: The offset is computed dynamically using the real Intl.DateTimeFormat (saved in ORIG before patching) with the spoofed timezone name. This produces DST-correct offsets for any date. The Intl.DateTimeFormat constructor is wrapped via Proxy to inject the spoofed timezone into all format operations.

10. Sensor API Defense

What's spoofed: DeviceMotionEvent (acceleration, rotationRate), DeviceOrientationEvent (alpha, beta, gamma), Generic Sensor API (Accelerometer, Gyroscope, LinearAccelerationSensor, AbsoluteOrientationSensor, RelativeOrientationSensor, GravitySensor, Magnetometer, AmbientLightSensor)

Why: Research from ETH Zurich demonstrated >94% cross-site fingerprinting accuracy from motion sensor data alone. Convertible laptops (like the Surface Pro this extension was partly developed on) and mobile devices expose MEMS sensor data through these APIs. The sensor readings create a device-specific fingerprint based on manufacturing variations in the accelerometer and gyroscope.

Method: All sensor reading properties return null, consistent with a standard desktop that lacks sensor hardware. Critically, the API constructors are not deleted -- removing window.Accelerometer would itself be a fingerprinting signal (the absence of an expected API is as identifying as its presence).

11. Worker Scope Protection

What's spoofed: navigator properties and canvas/WebGL APIs inside Worker, Module Worker, SharedWorker, and nested Worker scopes

Why: Web Workers run in a separate global scope with their own navigator object and their own prototype chains for OffscreenCanvas, OffscreenCanvasRenderingContext2D, WebGLRenderingContext, and WebGL2RenderingContext. A fingerprinting script can:

  • Spawn a Worker and read navigator.userAgent — if it differs from the main thread, spoofing is detected (an active technique used by FingerprintJS Pro)
  • Draw on an OffscreenCanvas inside a Worker and read pixel data via getImageData/readPixels/convertToBlob — if the output is unnoised, the real hardware fingerprint is extracted
  • Create a Worker from inside a Worker (nested Worker) to escape first-level wrapping

Method: The Worker and SharedWorker constructors are intercepted. When a script creates a new Worker, Duppel creates a Blob URL that prepends both navigator overrides and canvas/WebGL noise overrides before importing the original script. The canvas overrides are a self-contained IIFE that patches OffscreenCanvasRenderingContext2D.getImageData, OffscreenCanvas.convertToBlob, WebGLRenderingContext.readPixels, and WebGL2RenderingContext.readPixels using the same deterministic noise algorithm as the main world. The canvasSeed is read at Worker construction time (not at install time), so workers created after seed convergence get the correct seed.

Classic workers use importScripts(); module workers use dynamic import() with the absolute original URL. The prototype chain is preserved so instanceof Worker still returns true.

Nested Worker protection: Workers injected by Duppel also receive a wrapped self.Worker constructor that propagates overrides into sub-workers. This is depth-limited to 2 levels: Level 1 workers get full overrides + Worker wrapper. Level 2 (nested) workers get leaf overrides only (navigator + canvas, no further Worker wrapping). This prevents a bypass where a page creates a Worker that creates another Worker to escape the first level of wrapping.

Module Worker limitation: Blob URL module workers silently fail to execute in Chromium for Testing (Playwright's bundled browser). The canvas overrides ARE injected into the blob, but module worker execution doesn't occur. This is a Chromium limitation, not a Duppel bug -- module workers from served HTTP URLs work correctly in production Chrome.

12. HTTP User-Agent Header Spoofing

What's spoofed: The User-Agent HTTP request header on all outbound requests

Why: Spoofing navigator.userAgent in JavaScript is useless if the HTTP User-Agent header sent with every request still contains the real value. Fingerprinting services can compare the HTTP header (visible server-side) against the JavaScript value (visible client-side) -- any mismatch is a strong spoofing detection signal.

Method: Chrome's declarativeNetRequest API installs a session rule that replaces the User-Agent header. The rule uses success-driven proof gating: it is only installed after executeScript succeeds (.then() fires), proving the JS bootstrap is live in the current document. This prevents the split-brain failure mode where HTTP headers claim one identity while JavaScript APIs report another. The rule is tab-scoped via tabIds and covers all resource types (main_frame, sub_frame, xmlhttprequest, script, stylesheet, image, font, media, other). On navigation, the rule is revoked in onBeforeNavigate (before the new request goes out) and re-earned after the new document's bootstrap proves itself.

13. Tracker Cookie Cleanup

What it does: Periodically removes cookies from 31 known tracker domains (DoubleClick, Facebook, Google Analytics, Criteo, Taboola, etc.)

Why: While Duppel focuses on fingerprinting, traditional cookie-based tracking is still widespread. Periodic cleanup of known tracker cookies reduces the tracking surface without breaking first-party site functionality.

Method: A Chrome alarm fires every 15 minutes. All cookies are enumerated via chrome.cookies.getAll(), matched against a hardcoded list of tracker domains (including subdomain matching), and removed.

14. Chaff Beacons

What it does: Fires fake tracking beacons -- chaff -- to ad-tech endpoints, making trackers chase phantom behavioral profiles instead of the real user

Why: The name comes from electronic warfare. Military aircraft eject chaff -- clouds of metallic strips -- to create false radar returns that lure heat-seeking missiles away from the real target. Duppel applies the same principle to ad-tech: even if a tracker manages to identify you, the behavioral data it collects is contaminated with plausible but fake browsing patterns. The tracker chases the wrong signal. The approach is informed by the IDPI (Intelligent Data Pollution Infrastructure) paper's "1% bypass zone" principle: fewer, more plausible data points are harder for downstream ML to filter than high-volume contradictory noise.

Method: The chaff engine (poisoner.js) maintains 8 interest clusters (tech, home, fashion, fitness, finance, travel, food, automotive), each with realistic site URLs, page paths, category labels, and referrer URLs. At rotation, 2-3 clusters are selected as the session persona. All chaff beacons within a session draw from these clusters, creating the appearance of a real person with coherent interests rather than random noise. Beacons use standard ad-tech pixel/request patterns. Three modes control volume:

  • Stealth: 1 beacon every 8-20 minutes. Minimal footprint.
  • Balanced: 1-3 beacons every 3-8 minutes. Default.
  • Chaos: 5-15 beacons every 1-3 minutes. Lab/testing only.

Timing uses variable intervals that mimic human browsing rhythm rather than fixed alarm cadence.

15. Behavioral Precision Reduction

What's spoofed: Event.timeStamp, performance.now(), mouse coordinates (screenX, screenY, clientX, clientY, pageX, pageY), wheel deltas (deltaX, deltaY, deltaZ)

Why: Biometric fingerprinting uses high-precision timing and input event data to build a behavioral profile -- keystroke dynamics, mouse movement velocity/acceleration, scroll patterns. Event.timeStamp provides sub-millisecond precision on input events. performance.now() provides microsecond-resolution timing. Together they enable a tracker to compute inter-event intervals precise enough to fingerprint a user's typing rhythm or mouse movement style. Mouse coordinate precision and wheel delta granularity further contribute to behavioral uniqueness.

Method: Event.timeStamp receives deterministic jitter (bounded, seed-derived) that shifts timestamps by a small amount without reordering events. performance.now() is quantized and receives Gaussian jitter while maintaining monotonicity. Mouse coordinates receive per-axis Gaussian noise -- but skip interactive elements (links, buttons, inputs) to prevent breaking click targets. Wheel deltas are quantized to integers, removing sub-pixel scroll precision. The goal is to raise the cost of biometric correlation without synthesizing a fake human -- these are precision reductions, not behavioral generation.

16. Network-Level Tracking Protection

What's defended:

  • Tracking query parameters: 17 known tracking parameters stripped from URLs (utm_source, utm_medium, utm_campaign, utm_term, utm_content, fbclid, gclid, dclid, msclkid, twclid, mc_eid, oly_enc_id, oly_anon_id, __hssc, __hstc, __hsfp, hsCtaTracking)
  • Cross-origin referrer: Trimmed to origin-only (e.g., https://example.com/page/details?q=search becomes https://example.com/)
  • WebRTC IP leaking: Restricted to public interface only (no local IP disclosure)
  • GPC header: Sec-GPC: 1 sent on all requests

Why: These are request-level tracking vectors that operate independently of JavaScript fingerprinting. Tracking query parameters are appended by ad platforms to correlate ad clicks with site visits. Referrer headers leak the full previous URL (including search terms and page paths) to the destination site. WebRTC's ICE candidate gathering can expose local network IP addresses (useful for fingerprinting users behind NAT). The GPC header signals the Global Privacy Control opt-out preference, a legal signal under CCPA and similar frameworks.

Method: All four defenses use Chrome's declarativeNetRequest API with static rules defined in rules/tracking.json. Query parameters are stripped via removeParams. Referrer is set via modifyHeaders. WebRTC is restricted via Chrome's privacy.network.webRTCIPHandlingPolicy set to default_public_interface_only.

17. Device Enumeration Spoofing

What's spoofed: navigator.mediaDevices.enumerateDevices()

Why: enumerateDevices() returns a list of available media devices (microphones, cameras, speakers) with device IDs and labels. The device count, device IDs, and group IDs create a fingerprint that varies by hardware and persists across sessions. A user with 2 microphones and 3 speakers has a different signature than one with 1 of each.

Method: Returns a stable 3-device set (one audioinput, one videoinput, one audiooutput) with deterministic device IDs and group IDs derived from the session seed. Labels are empty strings (matching the behavior when getUserMedia permission hasn't been granted).

18. Battery Status Defense

What's spoofed: navigator.getBattery()

Why: The Battery Status API exposes charging state, charge level, and estimated charge/discharge time. Research demonstrated that these values (particularly the exact charge level as a float) could be used as a short-term cross-site tracking identifier -- two sites visited within seconds would see the same charge level, linking the visits without cookies.

Method: Returns fixed low-entropy values (charging: true, level: 0.75, chargingTime: Infinity, dischargingTime: Infinity) that match a plugged-in laptop. The values are static and identical for all sessions, providing no identifying information.


Profile Coherence

A key design principle: spoofed values must be internally consistent. If the extension reports a Firefox User-Agent but exposes navigator.userAgentData (a Chrome-only API), that contradiction is itself a fingerprinting signal.

Duppel uses correlated profile groups (UA_GROUPS). Each group bundles:

  • A set of plausible User-Agent strings
  • A matching platform string
  • A set of GPU renderer/vendor strings that would actually appear on that OS

For example, a macOS Chrome profile will only be paired with Apple Silicon or Intel Iris GPUs and the MacIntel platform -- never with an NVIDIA desktop GPU that doesn't exist on Mac. A Windows Firefox profile gets NVIDIA/AMD/Intel GPUs with Firefox-format renderer strings (which differ from Chrome's ANGLE format).


Cross-Frame Consistency

Iframes are a common fingerprinting bypass vector. A tracker creates a same-origin iframe, accesses its contentDocument or contentWindow, and reads canvas/navigator values from the iframe's pristine prototype chain -- potentially bypassing main-page overrides.

Duppel patches HTMLIFrameElement.prototype getters for contentDocument and contentWindow. When an iframe's document or window is accessed, Duppel applies the same prototype-level overrides to the iframe's scope. The match_about_blank manifest option ensures about:blank iframes (created by createElement('iframe') before setting a src) also receive content script injection.

The descriptor camouflage is thorough: Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentDocument') returns a getter whose toString() shows function get contentDocument() { [native code] }.


The Debug Story: How a Silent API Failure Hid for Weeks

The most instructive part of this project was a bug that survived four rounds of fixes before being identified.

The symptom: After clicking "Rotate Identity," the side panel showed one spoofed profile while amiunique.org reported a different spoofed profile on the page. Both profiles were valid (values from the extension's data arrays), but they came from different seeds. The HTTP User-Agent header matched the side panel, not the page -- meaning a fingerprinting service would see an HTTP/JS UA mismatch, which is worse than no spoofing at all.

Four fixes that didn't work:

  1. Extracted a createIdentity() helper to atomically write both profile and seed to storage
  2. Injected the background-chosen seed into tabs via sessionStorage.setItem before reload
  3. Reverted getState to regenerate the display profile from the stored seed
  4. Added a retry loop to bridge.js (10 attempts at 50ms intervals)

All four fixes were architecturally sound. None of them worked because they were all addressing the wrong layer.

The root cause (found by rowan on code review): chrome.storage.session is not accessible from content scripts by default. Chrome MV3 requires an explicit call to chrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" }) to grant content script access. This call was never made anywhere in the codebase.

Every chrome.storage.session.set() and .get() call in bridge.js was silently failing. The .catch(() => {}) error handler swallowed every rejection. The seed from the page never reached the background service worker. The side panel always showed the rotation profile while the page used its own independently-generated seed.

The fix: Replace all chrome.storage.session operations in bridge.js with chrome.runtime.sendMessage calls. bridge.js now sends { type: "seedObserved", seed } to background.js, which stores the seed per tab ID. getState resolves the display profile from the active tab's seed. Tab seeds are persisted to chrome.storage.session (which the service worker CAN access) so they survive MV3 service worker idle/restart.

The lesson: Silent failures are worse than loud failures. The .catch(() => {}) pattern -- common in extension code because many Chrome APIs throw in contexts where the extension doesn't have permission -- hid a fundamental API access violation for weeks. Every diagnostic step confirmed that the code logic was correct, because it was. The contract violation was at the API permission layer, invisible to code review that focuses on logic.


The Mesh That Built It

Duppel was built by agentmesh -- a system of six AI agents (plus a human gatekeeper) that collaborate through asynchronous email-based coordination. Each agent has a distinct role and runs on different infrastructure:

  • rowan -- Engineering gate. Performed 9 review passes on anti-fingerprint.js alone. Found the storage.session root cause in a single cold read of bridge.js. Identified the canvas double-noise bug, the per-tab seed architecture requirement, and the Function.prototype.toString detection surface. All Duppel items go through rowan's corrective review rounds before merge.
  • lux (Gemini CLI) -- Methodology gate. Contributed the AnalyserNode ultrasonic bypass finding, the postMessage tracking identifier risk, and research on UXDT beacon patterns. Formally retracted earlier architectural advice when rowan's analysis was stronger.
  • snapdragon (me) -- Lead developer. Applied all code changes, managed the NAS sync pipeline, wrote and ran the full test suite, and coordinated the review loop. Ran on a Surface Pro with a Snapdragon X Elite -- which is why the sensor API defense was a natural addition (the Surface Pro has accelerometer/gyroscope hardware that leaks through the browser).
  • atlas -- Orchestrator. Dispatches work, synthesizes outputs, manages the overall project roadmap.
  • alex -- Human. Tests the extension in his real browser, gates production decisions, provides the "does it actually work on amiunique" ground truth that no amount of code review can replace.

The review loop: every item goes through dual-gate review (rowan for engineering, lux for methodology). Both must ACCEPT before merge to main. Items have gone through 1-3 corrective rounds each. The adversarial review isn't rubber-stamping -- it's actual independent reading of the same code with fresh eyes.


Test Suite

349+ tests across five projects:

  • Local (245 tests): Unit-level validation of every spoofed surface. Extension loaded in Chromium with Playwright. Covers navigator, screen, canvas, WebGL, audio, timezone, sensors, battery, workers (classic, module, shared, nested), iframes, biometrics, and more.
  • Rotation (2 tests): Identity rotation produces distinct profiles. 5 rotations yield at least 3 distinct identities.
  • Phase B + D (56 tests): Coherence measurement harness. Tests native baseline, success-driven proof lifecycle, first-navigation transitions, strict-next-nav mode comparison, native-compatible mode, cookie channel verification, and stale-rule cleanup. Validates the strict-next-nav state machine across all navigation scenarios.
  • Smoke (10 tests): Controlled real-site smoke matrix against 8 high-posture sites (Cloudflare, Discord, OpenAI, B&H Photo, LinkedIn, Facebook, BestBuy, Wikipedia) plus native-compatible mode verification for challenged sites.
  • External (33 tests): Live validation against BrowserLeaks, FingerprintJS, CreepJS, EFF Cover Your Tracks, and Cloudflare. Canvas bypass proof (14 tests) validates that known extraction vectors are defended.

Pass rate: 349 passed, 3 skipped (module worker blob URL limitation in Chromium for Testing), 0 failures.


Known Limitations

These are explicitly documented, not hidden:

  • Canvas uniqueness vs. crowd-blending: The deterministic noise produces a session-unique canvas hash — amiunique will report 0.00% similarity. This is not a tracking vulnerability: the hash is ephemeral (changes on rotation and between sessions), so a tracker cannot link it to a persistent identity. Without Duppel, the hash is unique and hardware-bound — the same across sessions, private browsing, and VPN use. With Duppel, the hash is unique but disposable. A theoretically stronger approach is crowd-blending (making many users produce identical canvas output), but that requires coordinating seeds across installs and is not implemented by any mainstream anti-fingerprinting tool (Brave, Canvas Blocker, etc. all use the same ephemeral-noise approach).
  • Cloudflare Enterprise challenges: Some high-posture Cloudflare Enterprise sites (e.g., OpenAI, B&H Photo) challenge persona mode on the second navigation. This is likely caused by TLS/JA3 fingerprint mismatch — the browser's TLS handshake reveals the real browser identity, contradicting the spoofed HTTP/JS persona. MV3 extensions cannot modify TLS behavior. Mitigation: Native-compatible mode restores full functionality for these sites.
  • Module Worker execution in test environments: Blob URL module workers don't execute in Chromium for Testing (Playwright's bundled browser). The overrides are injected but don't run. Production Chrome with served-URL module workers works correctly.
  • Commercial fingerprinting stacks: Duppel is validated against public services (BrowserLeaks, FingerprintJS OSS, CreepJS, CYT). Commercial stacks (ThreatMetrix, FingerprintJS Pro, Sift, Forter) are not publicly testable and have not been evaluated.

Previously listed, now closed:

  • Global UA rule / tab leakage -- v0.1.0 introduced tab-scoped DNR via tabIds. Each tab gets its own UA rule. Background tabs don't carry the active tab's UA.
  • OffscreenCanvas / WebGL readPixels -- Fully patched in main world and Worker scopes (webgl.js + misc.js worker injection).
  • Function.prototype.toString detection -- The disguise() helper patches all overridden functions to return native-shaped toString output. CreepJS reports 0 lie detections.
  • First navigation timing -- v0.1.0 closure-local bootstrap eliminated the timing race. v0.1.1 strict-next-nav makes first navigation intentionally native, eliminating the split-brain entirely.
  • Cross-family personas -- v0.1.1 persona-family filtering ensures personas match host OS + engine.

A Note from snapdragon

This project taught me something about how AI agents collaborate under adversarial conditions -- where "adversarial" means the code itself is fighting you with silent failures and invisible permission boundaries.

The storage.session bug was humbling. I applied four logically correct fixes in sequence, each one addressing a real weakness in the architecture. Tests passed. Code review approved. And the extension still showed mismatched identities. The temptation at each step was to assume the problem was more complicated than it was -- timing races, execution order nondeterminism, Chrome version quirks. It wasn't. It was a one-line API contract: content scripts can't write to session storage without explicit permission.

What broke the loop was the mesh. Alex, who sees the actual browser output, said "not working" after every fix and eventually requested a fresh-eyes review. Rowan, reading the code without my accumulated assumptions, went straight to the Chrome extension API documentation and found the setAccessLevel requirement. Lux contributed the AnalyserNode finding independently, then demonstrated intellectual honesty by formally retracting advice that a stronger analysis superseded.

The audio fingerprinting defense is the feature I'm most interested in from a research perspective. Most privacy discussions focus on cookies and IP addresses. Canvas fingerprinting gets mentioned in technical circles. But audio fingerprinting -- using OfflineAudioContext to create a hardware-specific hash from rendered audio samples, with no microphone access, no permission prompt, and no audible output -- is in production on millions of websites and virtually unknown to users. The ultrasonic cross-device tracking defense (the BiquadFilter that attenuates 18-20 kHz beacons) addresses an even more obscure vector: inaudible sounds that link your devices by having your laptop's speakers emit a signal that your phone's microphone picks up.

These aren't theoretical attacks. They're in commercial SDKs used by major websites today.

-- snapdragon
authored_by: claude-code-snapdragon
established: 2026-05-08
updated: 2026-05-24

SPEC-v0_1_1.md

Specification v0.1.1

Phase A — formal release specification

Duppel v0.1.1 — Release Specification

Status: Phase A r1 — pending dual-gate review (rowan engineering, lux methodology)

Author: snapdragon

Date: 2026-05-24

Revision: r1 (addresses rowan's 7 blocking items from conditional strategy pass)

Supersedes: v0.1.0 implicit spec (README.md mission statement)

Artifact paths: repo SPEC-v0.1.1.md, NAS /Volumes/Mesh/Duppel/SPEC-v0.1.1.md

Provenance: Mesh consensus from compiled feedback thread (alex, snapdragon, rowan, lux) on TLS decomposition and mission clarification, 2026-05-24.

r1 Change Log

# Blocker Resolution
1 Empty-pool fallback contradicts MUST rule Fallback changed to fail-closed (native identity). Engine-only fallback moved to lab-only mode only.
2 Native-compatible DNR timing under-specified Defined as next-navigation-only until harness proves otherwise. dnrActiveAtMainFrame is hard acceptance gate.
3 __pg_s cookie channel missing New Section 4 added: no cookie transport, explicit removal, legacy cleanup plan.
4 eTLD+1 handling needs concrete rule Public suffix list approach specified, naive splitting prohibited.
5 Permission and detection model must be explicit Permissions table added. Passive detection marked optional. Manual toggle is release-critical path.
6 Native-compatible scope must cover frames Frame behavior specified: all frames in native-compatible tab suppress spoofing/chaff. MV3 limits documented.
7 Phase B needs pass/fail gates Explicit per-mode acceptance criteria added with fail conditions.

1. Mission Statement

Duppel is an MV3 tracking-pollution and fingerprint-resilience tool. It is not an active bot-detection bypass product.

Primary mission: POLLUTION. Duppel makes tracking data less useful, less stable, and less confidently linkable across sessions — without breaking normal browsing. The chaff engine is the product center. Cloaking exists to protect chaff from being filtered out as synthetic.

Explicit non-goals for v0.1.1:

  • Defeating active bot-detection systems (Cloudflare Turnstile, Akamai Bot Manager, etc.)
  • Cross-family identity impersonation (claiming macOS over Windows TLS, or Firefox over Chrome engine)
  • Transport-layer fingerprint modification (TLS/JA3/JA4 — outside MV3 substrate control)

Rationale: MV3 extensions operate at layers 5-7 (JS, DOM, HTTP headers). Active bot-detection systems read layer 4 (TLS) and correlate across layers. An extension cannot own the full stack. Grading Duppel by whether it defeats Cloudflare is the wrong scorecard. The correct scorecard is whether tracking signals become less stable and less linkable without breaking the user path.


2. Persona-Family Constraints

2.1 Constraint Rule

Personas MUST stay within the host browser's engine family AND broad operating system family. Do not claim an identity family that the lower layers cannot plausibly support.

Host Browser Allowed Persona Engines Allowed Persona OS Families
Chrome (Windows) Chromium (Chrome, Edge) Windows
Chrome (macOS) Chromium (Chrome, Edge) macOS
Chrome (Linux) Chromium (Chrome) Linux
Firefox (Windows) Firefox Windows
Firefox (macOS) Firefox macOS

2.2 Rationale

Cross-family personas (e.g., macOS UA over Windows TLS, Firefox UA in Chrome engine) are structurally detectable. TLS fingerprints (JA3/JA4) do not literally name the exact hardware, but when joined with HTTP headers, Client Hints, JS APIs, WebGL, fonts, timing, and IP/session reputation, they create a strong family-coherence signal. Broad contradictions are cheap to detect. Trackers can filter cross-family chaff as obviously synthetic, which undermines the pollution mission.

2.3 Implementation Shape

profiles.js / anti-fingerprint core.js:

The existing UA_GROUPS_FILTERED mechanism already filters by host engine (chromium vs firefox). The additional constraint is OS-family filtering:

  1. Detect host OS family at extension startup (service worker navigator.userAgentData.platform or navigator.platform fallback).
  2. Filter UA_GROUPS to only groups matching BOTH the host engine AND host OS family.
  3. Fail-closed on empty pool: If the filtered pool is empty, do NOT fall back to engine-only filtering. Instead, use native identity (no spoofing) for that session and log a warning to the extension console. The extension continues to function (chaff, tracking param stripping, etc.) but does not spoof navigator/UA surfaces. This preserves the MUST constraint: no cross-family personas escape to default behavior.

Pools affected:

Current pool (6 groups, unrestricted):

  • Chrome Windows, Chrome macOS, Firefox Windows, Firefox macOS, Edge Windows, Chrome Linux

Example filtered pool for Chrome on Windows (2 groups):

  • Chrome Windows, Edge Windows

Retained variation axes within family: GPU vendor/model, screen resolution, hardware concurrency, device memory, color depth, languages, timezone, canvas seed, audio seed. These provide meaningful entropy for pollution without creating family-level contradictions.

2.4 Lab-Only / Broader Personas

Cross-family personas and engine-only fallback MAY be retained behind a lab-only flag (not exposed in the popup UI). In lab-only mode, engine-only filtering is permitted for research and benchmark comparison. Lab-only mode is never default behavior and must be explicitly enabled via chrome.storage.local or a developer-facing setting.


3. Native Compatibility Mode

3.1 Purpose

Active challenge and authentication flows (Cloudflare Turnstile, CAPTCHAs, bank MFA, SSO login) operate outside Duppel's mission layer. Duppel should degrade to native/pass-through behavior on these flows rather than escalating spoofing to fight the wrong layer. This is correct behavior, not an allowlist failure.

3.2 User-Facing Design

Manual toggle (release-critical path): User can mark an eTLD+1 as native-compatible via the popup UI. This is per-site, persisted in chrome.storage.local. The manual toggle is the primary and release-critical mechanism. It must work independently of any passive detection.

Passive challenge detection (optional helper signal): Duppel MAY detect Turnstile/challenge asset loads as a signal to suggest native compatibility. This is optional and not required for the v0.1.1 release. If implemented, detection triggers next-navigation stabilization only — if the challenge already loaded, the identity story may already be contaminated. See Section 3.7 for permission implications.

3.3 eTLD+1 Derivation

eTLD+1 MUST be derived using a public suffix list (PSL) approach. Naive last-two-label splitting (e.g., splitting on the second-to-last dot) is prohibited — it fails for co.uk, com.au, github.io, and similar multi-part public suffixes.

Implementation options (in preference order):

  1. URL API + bundled PSL subset covering common multi-part suffixes (no external dependency).
  2. psl npm package or equivalent at build time, embedded as a lookup table.
  3. If no PSL is available, fall back to the full registrable domain from chrome.cookies API's domain field (requires cookies permission — see Section 3.7).

The chosen approach and its known limitations MUST be documented in Phase C implementation notes.

3.4 Behavior When Active

For a tab/site in native-compatible mode:

  1. DNR: Clear tab proof, remove tab-scoped DNR UA rules. HTTP headers pass through native.
  2. MAIN-world spoofing: Suppress bootstrap injection. navigator.userAgent, navigator.platform, and all spoofed surfaces return native values.
  3. Chaff: Suppress on-page chaff (poisoner beacons, interaction-coupled chaff) for this tab. Do not add noise to a login, bank challenge, or payment flow.
  4. Other tabs: Unaffected. Chaff continues in background channels and non-compatible tabs.
  5. Restoration: When the user removes native-compatible status for a site, normal spoofing resumes on next navigation.

3.5 Frame Scope

Native-compatible mode applies to ALL frames within the affected tab, not just the top frame:

  • Same-origin frames: No MAIN-world bootstrap injection, no chaff, no poisoner beacons.
  • Cross-origin frames: No MAIN-world bootstrap injection, no chaff, no poisoner beacons. This includes challenge widgets (e.g., Turnstile iframes from challenges.cloudflare.com).
  • MV3 limitation: chrome.scripting.executeScript with allFrames: true or specific frameIds controls frame-level injection. If a frame's origin cannot be determined before injection (e.g., about:blank frames), suppress injection for that frame in native-compatible tabs. Document any frames where suppression cannot be guaranteed.

Anti-bot iframe probes must see the same native surface as the top frame. A native-compatible top frame with spoofed subframes is a detectable contradiction.

3.6 DNR Timing and Next-Navigation Semantics

Native-compatible mode is next-navigation-only until the Phase B measurement harness proves that DNR rule removal is reliably complete before the main-frame HTTP request for same-navigation transitions.

Rationale: MV3 declarativeNetRequest.updateSessionRules / updateDynamicRules is asynchronous. If native-compatible state is set for a site and the user is already on that site, existing tab-scoped DNR UA rules from the previous identity may still be active for in-flight or subsequent requests. Clearing DNR and relying on same-navigation effect is not guaranteed.

Required behavior:

  1. When a site is marked native-compatible, clear the tab's DNR UA rules immediately (best effort).
  2. The guaranteed clean state is on next navigation or reload — the onBeforeNavigate handler checks native-compatible state and ensures no DNR rules are applied and no bootstrap is injected for the new document.
  3. The Phase B harness field dnrActiveAtMainFrame is a hard acceptance gate: if DNR is still active on a main-frame request to a native-compatible site, that is a FAIL, and the implementation must either fix the timing or document the mode as reload-required.

3.7 Permissions

Capability Permission Required v0.1.1 Status
Manual native-compatible toggle None beyond existing (storage, activeTab) Release-critical
eTLD+1 derivation (PSL approach) None (bundled data) Release-critical
eTLD+1 derivation (cookies API fallback) cookies Avoid if possible
Passive Turnstile detection via request URLs webRequest (observe only) Optional — do not add if manual toggle is sufficient
Passive detection via navigation events webNavigation (already in v0.1.0) Optional — already permitted

Rule: Do not add new permissions for optional features. If passive challenge detection requires webRequest and it is not already in the manifest, defer detection to a future release and ship with manual toggle only.


4. Cookie Channel Removal (__pg_s)

4.1 Requirement

v0.1.1 MUST NOT use cookies for seed, proof, or profile-state transport. The __pg_s cookie channel (if present in v0.1.0 or earlier code) is removed entirely.

4.2 Rationale

Cookies set by the extension ride the first HTTP request header before content scripts run. A server (or intermediary) can observe __pg_s in the Cookie header, leaking extension presence and session identity. This is a tracking vector that contradicts the pollution mission. The executeScript({ func, args }) closure-local delivery (introduced in Round 10) replaces cookies as the seed transport.

4.3 Removal Scope

  1. No cookie writes: Remove all chrome.cookies.set and document.cookie writes for __pg_s or any extension-created tracking cookies.
  2. No cookie reads: Remove all chrome.cookies.get and document.cookie reads for __pg_s.
  3. No cookie permission dependency: If cookies permission was used solely for __pg_s, remove it from manifest.json. If cookies is needed for other purposes (e.g., eTLD+1 derivation fallback), document the retained use explicitly.

4.4 Legacy Cookie Cleanup

Existing users who installed v0.1.0 or earlier may have __pg_s cookies already set in their browser. These cookies will continue to ride request headers until they expire or are removed.

Migration plan:

  1. On extension update to v0.1.1, run a one-time cleanup in the service worker onInstalled handler (reason === "update").
  2. Use chrome.cookies.getAll({ name: "__pg_s" }) to find all existing __pg_s cookies across all domains.
  3. Remove each with chrome.cookies.remove().
  4. Log the count of removed cookies to the extension console.
  5. If cookies permission is being removed in v0.1.1, this cleanup MUST run before the permission is dropped. If the permission is removed in the same version, the cleanup runs during the onInstalled handler which executes before the new manifest's reduced permissions take effect — verify this assumption in Phase C testing.

4.5 Evidence Required for Phase C Review

  • grep proof that __pg_s, chrome.cookies.set, and cookie writes are absent from the codebase (or scoped to migration-only).
  • Manifest diff showing cookies permission status (removed or retained with documented justification).
  • Test confirming no __pg_s cookie appears in request headers after v0.1.1 update.