Reverse Engineering

addScriptToEvaluateOnNewDocument: Injecting Code Before Page Scripts

addScriptToEvaluateOnNewDocument: Injecting Code Before Page Scripts — conceptual illustration
On this page

addScriptToEvaluateOnNewDocument is a Chrome DevTools Protocol (CDP) command that registers JavaScript to run in the page's own context immediately after a document is created, before any of the page's own scripts execute. That timing is the whole point: to hook a function the page will call, your wrapper has to be installed first. Running before page script means you win the race and can replace built-ins while the page still thinks they are pristine.

Quick facts

ProtocolChrome DevTools Protocol (CDP), Page domain
When it runsAfter document creation, before page scripts
Where it runsThe page main world (not an isolated world)
ScopeEvery new document/navigation until removed
Common useInstalling pre-load hooks on built-ins

Winning the race against page script

A hook is only useful if it is in place before the function is called. Page scripts often fingerprint within milliseconds of load, so injecting after the fact is too late. addScriptToEvaluateOnNewDocument solves this by registering your source with the browser; on every new document the browser evaluates it first, in the page main world, before handing control to the page. This is how automation frameworks and instrumentation tools install their shims.

How you call it

You speak CDP over a WebSocket (or via a binding like Playwright/Puppeteer's addInitScript, which wraps this exact command). You send the command once with your script source; the browser replays it on every navigation until you remove the identifier it returns. Because it runs in the main world, your code can reassign HTMLCanvasElement.prototype.toDataURL and the page will call your version -- solving the isolated-world problem entirely.

What it does not solve

Pre-load injection wins the timing race but not the detection race. Two problems remain. First, the wrapper you install is still a JavaScript function that reveals itself via toString and property descriptors. Second, your script runs in the top frame, but sandboxed widgets such as Cloudflare Turnstile live in out-of-process iframes your injection never reaches. Observing a script without perturbing it needs engine-level hooks.

Code example

javascript
// Via Playwright (wraps Page.addScriptToEvaluateOnNewDocument):
await page.addInitScript(() => {
  const real = HTMLCanvasElement.prototype.toDataURL;
  HTMLCanvasElement.prototype.toDataURL = function (...a) {
    window.__calls ??= [];
    window.__calls.push({ api: 'toDataURL', args: a });
    return real.apply(this, a);
  };
});
// Runs in the page world, before page scripts -> the hook is in place
// when the fingerprinting collector calls toDataURL. (Still detectable
// by toString -- this only solves timing and world, not stealth.)

Related terms

Concept map

How Page.addScriptToEvaluateOnNewDocument connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Reverse Engineering
Building map…

Frequently asked questions

How is this different from a content script?

A content script runs in an isolated world and cannot touch page functions. addScriptToEvaluateOnNewDocument runs in the page main world, so your hooks actually affect the code the page runs.

Is using CDP observable by the page?

CDP attachment itself can be inferred through several signals, which is the subject of CDP detection. The injected script also remains observable as a non-native wrapper. So while it solves injection timing, it does not, on its own, let you observe a script without that script being able to notice.

Does addInitScript in Playwright use this command?

Yes. Playwright's addInitScript and Puppeteer's evaluateOnNewDocument are thin wrappers around Page.addScriptToEvaluateOnNewDocument, registering source the browser replays on every navigation.

Last updated: 2026-06-09