VulpineOS
Docs
Runtime
Runtime overview
Hardened browser, orchestrator, MCP
Injection filter
Strip invisible DOM before the agent
Action lock
Freeze JS, timers, reflows mid-think
Optimised DOM
93.1% measured token reduction
Operator surfaces
Web panel
Embedded operator console
TUI workbench
SSH-friendly terminal UI
MCP toolbelt
36 typed browser tools
Vulpine-Box
One-container self-host
Ecosystem
Foxbridge
CDP ↔ Juggler / WebDriver BiDi
vulpine-mark
Set-of-Mark visual labelling
mobilebridge
Android device sessions
OpenClaw
Pre-configured agent loop
Documentation
Quickstart
First agent in 30 seconds
Architecture
Four-phase security model
MCP Reference
All 36 browser tools
Agent Scripting DSL
Declarative agent definitions
Open source
VulpineOS Runtime
GitHub, MPL 2.0
Foxbridge
CDP ↔ Juggler / WebDriver BiDi
vulpine-mark
Set-of-Mark visual labelling
mobilebridge
Android device discovery + sessions
Integrations
OpenClaw
Pre-configured agent loop
Camoufox
Anti-detect browser core
LLM providers
30+ models, one toolbelt
Docker (Vulpine-Box)
One-click self-host
Guides
AI Agent Security
Threat model, mitigations
Prevent Prompt Injection
Hidden DOM, ARIA tricks
OpenClaw + Camoufox
End-to-end setup
Camoufox vs Chrome
Why Firefox, why patches
Engineering
Changelog
Release notes as work lands
Roadmap
Now, next, later, research
Blog
Long-form posts on the runtime
RenderLab
Optional render-diagnostics study
Community
Support
How to get help, fast
GitHub Discussions
Ideas, RFCs, support
Contributing
How to help
Security
Responsible disclosure
Star on GitHub →
← Back to blog9 May 2026 · Engineering

Why an agent runtime needs a docShell patch, not a freeze script

JS-on-JS freeze libraries are clever but cannot suspend the event loop from inside it. Action-lock is a C++ patch on nsDocShell — here's the protocol and what it prevents.

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:

  1. 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.
  2. 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.
  3. Layout reflows stop. CSS animations stop. Video elements stop. requestAnimationFrame callbacks 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

More postsBack to home
VulpineOS

The browser built for AI agents.
Open-source runtime, end to end.

Camoufox 146.0.1
Product
RuntimeAction lockOptimised DOMMCP toolbeltVulpine-Box
Ecosystem
Foxbridgevulpine-markmobilebridgeOpenClawCamoufox
Resources
Resources hubRoadmapChangelogBlogDocs
Community
SupportDiscussionsContributingIssuesSecurity
© 2026 VulpineOSBuilt on Camoufox · Firefox 146.0.1TermsPrivacyCookiesAcceptable useSecurity