Most "pause the page" libraries you'll find for browser agents are JavaScript-on-JavaScript. They override setTimeout, monkey-patch addEventListener, suspend MutationObservercallbacks, hold the next-tick queue with a busy loop. They're clever. They sometimes work in toy demos. And they all share one fatal flaw: you cannot suspend the JavaScript event loop from inside the JavaScript event loop.
This matters because the moment your agent reads the page and decides to click button #3, the page is still running. A timer fires, a modal pops in, an iframe re-paints, an observer reorders the DOM, an A/B test swaps the button label. The agent acts on a snapshot the page no longer matches. The action lands somewhere unintended, or doesn't land at all, or — worse — silently triggers a fraud-detection signal designed to catch exactly this pattern.
Action-lock is our answer. It's a C++ patch on nsDocShellin our Camoufox fork. When the agent thinks, the page doesn't think back.
What other "freeze" approaches actually do
Walk the existing landscape and you'll find roughly three categories of DOM-freeze.
Mutation observer pauses. A MutationObserver watches the DOM, your wrapper holds the callback in a queue, you replay it after the action lands. Layout still reflows under you. Timers still fire. Network handlers still mutate state. You've blinkered yourself, not the page.
JS API monkey-patching. Override setTimeout, setInterval, requestAnimationFrame, Promise.then. The page can't schedule new work — until it grabs an unpatched reference via new Function("return setTimeout")(), or runs in a worker, or already had a timer scheduled before you patched. You've slowed the page down, at best.
designMode tricks. Some agent stacks reach for design mode or <dialog>modal blocking to halt user interaction. The page's own scripts keep running. Detection scripts notice immediately.
All three categories share the same defect. They run in the same JavaScript context as the page. The page's script, by definition, has equal authority. Anything you can patch, the page can re-patch. Anything you can hold, the page can drop a microtask in front of.
The actual fix lives below the JavaScript layer
When Page.setActionLock({ enabled: true })arrives over the Juggler protocol, here's what happens inside Camoufox:
- The browser process walks every frame in the docShell tree and sets
allowJavascript = false. Any JavaScript currently mid-execution finishes the synchronous tail; nothing new can start. - We call
docShell.suspendPage(). This freezes the refresh driver, suspends timers and intervals, halts pending network callbacks from delivering to JavaScript, and suppresses event handling. - Layout reflows stop. CSS animations stop. Video elements stop.
requestAnimationFramecallbacks queue but do not fire. Pointer events are captured but not dispatched.
The page is alive — its DOM still exists, its memory hasn't been freed, the canvas it had open is still painted. It just cannot run code. There is nothing for the page to do because the C++ that would normally pump work onto it is also paused.
The protocol
// Before the agent decides
await page.send("Page.setActionLock", { enabled: true });
// → JavaScript disabled on every frame
// → docShell.suspendPage() halts the refresh driver
// → timers, network callbacks, events queued
// After the agent commits an action
await page.send("Page.setActionLock", { enabled: false });
// → page resumes immediately
// Navigation auto-releases — no need to flip the lock
await page.send("Page.navigate", { url: "https://example.com" });That's the entire surface. One toggle, deterministic, bidirectional. Navigation auto-releases the lock so the next page loads normally; you don't have to bracket every navigation with explicit unlock calls.
What this prevents in practice
A short list of failures we've seen action-lock catch on real workloads.
- Mid-decision modal pop-in.A site detects "user is looking at the page" via Intersection Observer and pops a newsletter modal after 1.2 seconds. With action-lock, the timer never fires; the agent's snapshot stays valid; the click lands on the right button.
- A/B test swaps.A page mounts two versions of a button via React effects. With action-lock, the effect that would have swapped them mid-decision can't run; the agent operates on the version it actually saw.
- Cursor-tracking detection. Some bot-detection scripts watch
mousemoveevents and reject sessions where the cursor doesn't drift naturally between actions. Action-lock means the page literally cannot observe the cursor between decision and action — the events queue, but nothing dispatches them until we resume. - Race conditions on form submit. A form's
onSubmitwaits for a debounced validator to finish. Without action-lock you race the validator. With action-lock, the validator queues, the submit doesn't fire prematurely, and the agent can verify state before unlocking.
The tradeoff
It's deliberate, not free. Holding the lock too long means a real user opening the same tab would see the page hang. We expose lock duration in the runtime audit log so operators can see exactly how long a page was frozen during each agent turn — and we set sensible per-tool defaults so agents can't accidentally hold the lock through a five-minute LLM call.
The other tradeoff is the C++ patch itself. Action-lock isn't possible from a WebExtension. It isn't possible from CDP without an extension to receive it. It isn't possible from any layer above the docShell. You need to patch nsDocShell.cpp (or the Chromium equivalent — but the Chromium equivalent has different IPC tradeoffs that make this harder to ship safely). The fork is the cost. We pay it because the alternative is best-effort freeze libraries that fail at the worst moment.
One of four
Action-lock is one of four operator-grade primitives we patch into Camoufox. The injection filter strips invisible DOM before the agent sees it. The optimised DOM exporter compresses the accessibility tree by 93.1% before it reaches the model. Trust warming runs human-realistic interactions on identities while they're idle. All four are MPL 2.0 and ship with the runtime.
If you've built a browser-agent stack and watched it lose to a 200ms timer firing at exactly the wrong moment, this is the pattern that fixes it.
→ Read the action-lock product page · Architecture deep-dive · Star on GitHub