Skip to content

Patching Chromium from source vs runtime injection: the two stealth philosophies

· 19 min read
Copyright: MIT
Two-path diagram contrasting a recompiled patched browser binary against runtime JavaScript injection

Ask anyone who has shipped a scraper against a modern anti-bot wall how they hide the automation, and you get one of two answers. Either they reach for a stealth plugin that runs a pile of JavaScript before the page loads, rewriting navigator.webdriver and a few dozen other properties on the way in. Or they run a browser somebody recompiled from source, where the lie is baked into the C++ and there is no JavaScript shim to find. These are not two settings on the same dial. They are two different theories of how to be invisible, and they fail in opposite ways.

The runtime-injection camp is cheap, portable, and instantly out of date. The recompile camp is expensive, slow to ship, and far harder to catch in the act. Almost everyone starts in the first camp because it costs nothing to try. The teams that stay in the game long enough end up in the second, or pay someone who lives there. This post is about why that drift happens, what each approach actually costs, and where the engine-level advantage stops being a free lunch.

The road map: first the shape of the fork itself, then a close look at what runtime injection leaks and why those leaks are structural rather than bugs you can patch out. Then the engine-level approach, using Camoufox and Brave as the two clearest public examples of what “patch the source” really means. Then the part nobody likes to talk about, which is the maintenance treadmill of tracking an upstream browser that ships every four weeks. A short detour through the hybrid middle ground that has grown up since 2024. And a closing argument about why the better technique is the one fewer people can run.

The fork: where the lie lives

Every stealth technique is answering the same question. A detection script is going to run inside the page and ask the browser things about itself. It will read navigator.webdriver, render to a canvas and hash the pixels, enumerate WebGL parameters, probe navigator.permissions, time how fast certain operations complete, and inspect the prototype chain of the functions it called to see whether anything has been tampered with. The browser answers. Stealth is the practice of controlling those answers without the act of control being one of the answers.

There are only a few places you can sit to intercept that conversation. You can sit above the browser, in the JavaScript that runs in the page before the detector does, and redefine the properties the detector will read. That is runtime injection. You can sit inside the browser, in the compiled C++ that produces those answers in the first place, and change them at the source. That is the engine-level patch. The whole argument between the two camps comes down to a single property of that choice: whether the interception itself is observable from inside the page.

Runtime injection Source patch injected JS shim detector script (same context) unmodified engine stock C++ answers shim and detector share one scope detector script (same context) patched engine lie compiled in answers are native by construction no JS layer to enumerate *Where the interception sits. On the left, the shim and the detector occupy the same JavaScript scope, so the detector can look up at the shim. On the right, the change is below the JavaScript boundary and there is nothing above the engine to find.*

That picture is the entire thesis. On the left, the detector and the thing hiding from it run in the same room. On the right, the detector is in a room whose walls were built to lie, and it has no window into the construction. Everything that follows is consequence.

Why runtime injection leaks by construction

Start with the canonical signal. A WebDriver-controlled browser exposes navigator.webdriver === true, and that boolean appears in essentially every commercial anti-fraud tag because it is the cheapest true positive in the business. The puppeteer-extra stealth plugin, the reference implementation of the injection approach, cannot simply assign navigator.webdriver = false. The property is defined on the Navigator prototype with configurable: false, so a plain assignment does nothing. The plugin instead calls Object.defineProperty to redefine the getter so it returns undefined. That works, in the narrow sense that the boolean now reads the way a human browser reads it.

But you have not removed a signal. You have added one. The act of redefining a native property leaves marks. The replacement getter is a JavaScript function, not a native one, and Function.prototype.toString on a genuine native getter returns a body of [native code]. The stealth plugin knows this and patches toString too, which means it now has to patch the toString of its own toString patch, and so on. This is the structural problem with injection stated plainly: each patch you apply is itself a new object in the page, with its own descriptor, its own prototype, its own string representation, and a detector that enumerates carefully will find the patch you used to hide the previous patch. The surface does not shrink as you add evasions. It grows.

The same recursion shows up at the protocol layer, and there it is worse because the leak is not even in the page. To drive Chrome, the automation library speaks the Chrome DevTools Protocol. To run page.evaluate, the library needs an execution context to evaluate in, and to learn about execution contexts it enables the Runtime domain by sending Runtime.enable. That single command has a side effect: the browser starts emitting Runtime.consoleAPICalled events, and the act of enabling the domain produces behavior that a few lines of page JavaScript can observe. Anti-bot vendors picked this up, and by the mid-2020s a stock Puppeteer or Playwright session would draw a challenge from the larger vendors even running on a clean residential IP on a real machine. The leak had nothing to do with the page content. It was the protocol announcing itself.

automation lib browser (CDP) detector JS Runtime.enable side effect observable change flagged *The Runtime.enable path. The detector never inspects the page for a shim. It watches for the behavioral fingerprint of the domain being enabled, which is upstream of anything the injected JavaScript can control.*

You can fix that specific leak, and the people behind Rebrowser did, by not enabling the Runtime domain automatically and instead creating isolated execution contexts whose IDs the page cannot guess, or by toggling the domain on and immediately off to grab a context ID without leaving it enabled. Patchright took a related route, applying its changes to Playwright’s source through AST manipulation rather than at runtime, moving init-script injection out of CDP entirely and into the network layer where it modifies the HTML response and the Content-Security-Policy header to slip a <script> in that self-deletes after running. These are real fixes. But notice what they are. They are not “inject more cleverly.” They are “stop relying on the part of the protocol that leaks,” which means rewriting the automation library itself. The moment your stealth work requires recompiling the tooling, you have already left the pure injection camp. You are halfway across the fork.

That is the pattern worth internalizing. Injection starts as a config flag and a plugin install. It survives contact with a real detector for about as long as it takes the detector vendor to ship a new check, because the injected layer is always sitting in the same scope as the thing looking for it. The fixes that actually hold are the ones that stop injecting and start patching. The related write-ups on the CDP addScriptToEvaluateOnNewDocument trap and detecting CDP through the Runtime.enable leak go through the specific mechanics; the point here is the direction the pressure pushes you.

What “patch the source” actually means

Now the other side. If the problem with injection is that the lie lives above the engine where the page can see it, the obvious move is to put the lie inside the engine. You take the browser’s source, you find the C++ that produces a fingerprintable answer, you change it so the answer is whatever you want, and you compile. The detector reads navigator.hardwareConcurrency and gets back a number that came straight out of compiled native code with no JavaScript getter in the path. There is no shim, because the value was never a shim. It was the build.

Two public projects make this concrete in different ways, and they are worth contrasting because they patch for opposite reasons.

Camoufox is a Firefox fork built for scraping and agent automation, and it is the clearest open example of the source-patch philosophy applied to evasion. Its patches live in a dedicated directory and get applied to a fetched Firefox source tree before compilation, producing a patched source that then builds into portable distributions. The interception happens in Firefox’s C++ implementation, so the spoofed values for navigator properties, screen and window dimensions, WebGL parameters and shader precision formats, AudioContext sample rate and channel count, font metrics, timezone and locale, and the rest come back looking native because they are native. There is no JavaScript hijack for a page to enumerate. Camoufox also patches Juggler, Firefox’s automation protocol, to give the automation driver its own isolated copy of the page, so the page cannot detect reads, getter access, or listeners attached by the driver. The fingerprint values themselves are not hard-coded to one machine; they come from BrowserForge, which samples from the real-world statistical distribution of device data so that a spoofed profile lands inside the population a detector expects rather than at some impossible point outside it. The recent builds ship hundreds of bundled presets covering Firefox versions 149 through 152.

Brave solves a different problem with the same kind of tool, and it is instructive precisely because Brave is not trying to look like a normal browser. Brave’s “farbling,” shipped to Nightly in 2020, adds small per-session, per-origin pseudo-random noise to fingerprintable outputs like canvas and Web Audio. A second canvas read returns a slightly different image, a different origin returns a different value, a fresh session returns a fresh value, and the noise sits below human perception but above the threshold a fingerprint hash can survive. The result is a browser whose fingerprint changes constantly, which is as useless to a tracker as no fingerprint at all. The mechanism is an engine patch in exactly the sense Camoufox’s is: it lives in the C++ that generates the pixel data, not in a content script. Two projects, opposite goals, same architectural choice, and both get the same benefit, which is that the page cannot see the seam.

detectable surface vs. number of evasions high none evasions applied → injection source patch *The structural difference, drawn. Each injected evasion adds a new object the page can inspect, so the surface climbs as you pile on patches. A source patch changes the answer without adding anything above the engine, so the surface stays flat.*

There is a second, quieter advantage to patching at the source, and it shows up in the corners that injection forgets. A JavaScript shim has to be re-applied in every execution context: the top document, every same-origin and cross-origin iframe, every Web Worker and Service Worker. Workers in particular are a graveyard for injection-based stealth, because the spoofing script that ran in the page does not automatically run inside a worker the page spawned, and a detector that re-reads navigator.hardwareConcurrency or the user-agent from inside a fresh worker can catch a value the page-level shim never reached. An engine patch does not have this problem. It changed the function that returns the value, and that function is the same function no matter which context calls it. The post on iframe and worker context fingerprinting covers exactly the gaps that engine patches close for free and injection has to chase context by context.

The treadmill nobody budgets for

So engine-level wins on detectability. If the argument ended there, everyone would ship a patched binary and the injection plugins would be a historical footnote. They are not, and the reason is the cost structure on the other side of the fork.

A patched browser is not a thing you build once. It is a thing you rebuild forever, because the browser it is patched against is a moving target that ships on a four-week cadence. Chromium and Firefox both push a new stable major roughly every four weeks now, and each release moves code around. A patch is a diff against a specific revision of specific files, and when upstream edits those files, the diff stops applying cleanly. Sometimes it is a trivial rebase. Sometimes the function you were patching was refactored out of existence and you have to find where the logic went. You are doing this across the whole patch set, every cycle, indefinitely, and if you fall behind you are shipping a browser whose version string is a fingerprint of its own, because a Chrome/121 that should have aged into Chrome/138 by now is itself the tell.

Then there is the build. Compiling Chromium from source is not a quick edit-compile loop. On a four-core Intel i7-7700, a 2024 Chromium build clocked in just shy of 24 hours. On a low-end Ryzen 5 with the right setup it dropped to around four hours, and a developer on high-end hardware with everything cached can get under an hour, but the honest baseline for someone standing up this pipeline is “an overnight build on a real machine, every time something material changes.” A full Chromium checkout wants on the order of 16 GB of RAM and the debug build alone eats around 17 GB of disk before you have produced anything shippable. Multiply that by the platforms you need: a Linux build, a Windows build, and a macOS build are three separate compiles, three separate toolchains, three separate sets of breakage when upstream shifts. Camoufox’s own build pipeline spells this out, with separate make package-linux, make package-windows, and make package-macos steps hanging off the same patched tree.

the source-patch maintenance loop repeats every ~4 weeks, per upstream release upstream new major rebase patches fix conflicts compile ×3 lin/win/mac re-test leaks regressions miss a cycle and the stale version string becomes its own fingerprint *The loop that does not end. Every upstream major restarts it, and the cost is paid in engineering hours, not just CPU time. This is the line item that pushes most teams back toward injection or toward paying a vendor.*

And the leaks do not stay fixed. A spoofed fingerprint is only good while it stays internally consistent, and consistency is fragile because the engine has thousands of cross-correlated outputs. Spoof a Windows user-agent but leave a Linux font list, or claim an Apple GPU string while WebGL reports a renderer that never shipped on that GPU, and you have manufactured an impossible browser that flags harder than an honest bot would. Camoufox’s own maintainer has been candid that a gap in maintenance let the project drift: a stale base Firefox version plus newly discovered fingerprint inconsistencies dragged its performance down until active development resumed. That is not a knock on the project. It is the honest shape of the work. The detector vendors are continuously probing for new inconsistencies, and every one they find is a patch you now owe, on top of the rebase you already owed, on top of the compile you already owed. The treadmill has three belts and they all run at once.

This is why engine-level “wins but rarely scales.” It scales fine for the handful of organizations that can fund a permanent browser-build team: the anti-detect vendors whose entire product is the patched binary, and a few well-resourced scraping operations. It does not scale to the median team that needs to get past a wall this quarter, because that team cannot stand up and indefinitely staff a Chromium release-tracking pipeline for one project. The economics are covered more fully in the note on anti-bot vendor economics, but the short version is that the cost of the patched-browser approach is fixed and recurring while the benefit is probabilistic, and that math only closes at scale.

The hybrid middle that grew up after 2024

Because the two ends of the fork are each unsatisfying on their own, the interesting work since around 2024 has been in the middle, and it is worth naming because it confuses people who think the choice is binary.

The middle ground patches the automation tooling rather than the browser engine, which is a much smaller and cheaper thing to maintain, and uses the browser more carefully rather than rewriting it. Patchright is the cleanest example. It does not recompile Chromium. It patches Playwright, applying its changes through AST manipulation of Playwright’s own source, and the changes are about how Playwright talks to a stock Chrome: stop using Runtime.enable, run code in isolated worlds created through Page.createIsolatedWorld, strip the automation-revealing command-line flags like --enable-automation, add --disable-blink-features=AutomationControlled, and inject init scripts by rewriting the HTML response at the network layer instead of through the CDP command that leaves a fingerprint. Rebrowser’s patch is in the same family, targeting the Runtime.enable leak specifically while leaving the browser binary alone. The undetected-chromedriver and nodriver projects sit nearby, patching the Selenium and driver side rather than the engine; the post on nodriver and undetected-chromedriver walks through what they touch.

These are cheaper than a source patch because Playwright and the drivers are small relative to Chromium and they do not need a four-hour compile to rebuild. They are stronger than naive injection because they remove the protocol-level leaks that pure injection cannot reach. But they do not get the full engine-level benefit. They are still running a stock browser, so anything that is fingerprintable about the stock browser itself, the canvas hash, the WebGL renderer string, the font list, is exactly what a real machine of that configuration would produce, which is fine if your machine’s real fingerprint is unremarkable and a problem if you need to present as a different device than the one you are on. To spoof the device convincingly without injecting JavaScript that the page can catch, you are back to patching the engine. There is no free version of that.

So the map is really three regions, not two. Pure injection at the cheap end, fast to deploy and fast to die. Tooling-level patches in the middle, which close the protocol leaks for a manageable maintenance cost and are where a lot of 2026 scraping actually lives. And engine-level patches at the expensive end, which close the device-fingerprint surface too but only pay off if you can fund the treadmill. Most teams that think they need the expensive end actually need the middle, and most teams that start at the cheap end discover the middle the first time a real detector flags them.

Closing: the technique that wins is the one fewer people can run

The honest summary is uncomfortable for anyone who wants a tidy recommendation. Engine-level patching is the better technique on every axis that matters to detectability. The lie is below the JavaScript boundary, so the page cannot enumerate it. It reaches every execution context without re-application, so workers and iframes stop leaking. It produces native answers by construction, so there is no toString recursion and no descriptor that reads wrong. If you could get a freshly compiled, perfectly internally consistent patched browser for free on every upstream release, the injection plugins would have no reason to exist.

You cannot get it for free, and that is the whole story. The same property that makes a source patch undetectable, that it lives in code you had to compile, is the property that makes it expensive to keep alive. The detector vendors do not have to out-engineer the patch. They only have to keep the upstream browser moving and keep finding new inconsistencies, and the cost of staying current accrues entirely on the patcher’s side of the ledger. That asymmetry is why the strongest stealth is concentrated in a small number of vendors who can amortize a permanent browser-build team across many customers, and why everyone else either rents that work or settles for the cheaper layers and accepts a higher flag rate. The better philosophy did not lose to the worse one. It priced most of its practitioners out, and the ones who remain are precisely the ones who turned the maintenance treadmill into a business.


Sources & further reading

  • Camoufox (2026), Stealth Overview — vendor explanation of why C++-level interception leaves no JavaScript shim for a page to detect, plus the Juggler isolation approach.
  • daijro (2026), Camoufox — source repo showing the patch directory, the make dir/make build/make package-* pipeline, BrowserForge fingerprint sourcing, and the maintainer’s note on maintenance drift.
  • Brave Software (2020), Fingerprint randomization — the original farbling announcement, describing per-session per-origin noise added at the engine level to canvas, WebGL, and audio.
  • Rebrowser (2024), How to fix Runtime.Enable CDP detection — the Runtime.enable leak mechanism and the isolated-context and enable/disable fixes.
  • Antoine Vastel, Castle (2025), How to detect scripts injected via CDP in Chrome — detector-side techniques for catching CDP-injected scripts via scriptParsed events and memory profiling.
  • Octo Browser (2024), CDP leaks in Puppeteer — the pptr:evaluate Error.stack markers, debug-port scanning, and User-Agent override mismatches.
  • pim97 (2025), Patchright technical notes — how Patchright patches Playwright via AST manipulation, isolated worlds, flag sanitization, and network-layer init injection.
  • berstend, puppeteer-extra-plugin-stealth — the reference runtime-injection evasion set, including the navigator.webdriver and toString patches.
  • Chromium project, Checking out and building Chromium on Linux — official build prerequisites, RAM and disk requirements behind the compile-cost figures.
  • Bruce Dawson (2020), Big project build times — Chromium — measured Chromium build times across hardware, grounding the overnight-compile baseline.
  • Scrapfly (2025), Puppeteer Stealth: Complete Guide to Avoiding Detection — practical survey of what the stealth plugin patches and where injection-based evasion falls short.

Further reading