RemixletDocs

How it works

Security architecture

How the agent builds a remixlet, how the sandbox runs it, and how the dom and rmx APIs enforce what it can read, change, and send.

Remixlet has two runtimes. The agent builds a remixlet while you chat; the sandbox runs it whenever you visit a matching page. That separation is also the basis of its security architecture. The model can write code, but the extension decides what that code can reach, using a fixed page policy and the permissions you approved.

This matters because the agent reads websites where you are logged in, and those websites can contain instructions planted by someone else. To understand what protects you, start with where the work happens.

Two worlds#

The agent#

The agent lives in the side panel and runs only while you are chatting. It talks to the AI provider you connected and works on the one tab the chat started on. If that tab closes, the agent stops. Switching tabs does not move the conversation to another page.

Its tools capture the page, outline its structure, query elements, inspect computed styles, and examine the network resources the page loaded. It writes remixlet files and activates them through the extension's approval gate, which reloads the tab. With the beta "Check changes after writing" setting on, it then checks its work by clicking elements, asserting page state, reading the remixlet's log, and comparing the page with the change you asked for.

The agent has no tool for running arbitrary code on the page. Its probe parameters cross into extension code as data, never as a script. When the conversation ends, the agent stops working. The saved remixlet can keep running without another call to the model.

The sandbox#

A remixlet's JavaScript runs in a sandboxed extension page, loaded as an iframe in a document the extension keeps offscreen. Each remixlet gets its own sandbox for each page. A full navigation or tab close tears that sandbox down.

The sandbox has a null origin, no access to the site's cookies or storage, and no extension API. Its content security policy blocks direct network access and string evaluation. It never holds the page's document or its JavaScript state. Common browser globals such as document, fetch, and localStorage are shadowed with errors that point the author towards the supported APIs. Some globals, including window and location, have limited read-only replacements. These conveniences help the agent write correct code; the sandbox and the extension's message checks enforce the boundary.

Untrusted page
probes
Agent in side panel
AI provider
generates code
You approve sites and capabilities
Saved remixlet
Load on each matching page visit
Extension host
Remixlet sandbox
Remixlet code runs here
No direct DOM, network or extension access
Checks writes
Shared page DOM
Background worker
Checks grants
Approved hosts
Storage
Clipboard

Two extension components answer the running code. The page agent is a content script we ship in the page's isolated world. It holds the real document and checks requests to read or change it. The background worker handles privileged requests against the stored approval record. The offscreen host routes messages between them and the sandbox, attaching a secret token when it forwards a privileged request. Neither the remixlet nor the page receives that token.

The two APIs#

A remixlet uses dom to work with the page and rmx for extension capabilities and runtime helpers. Standard JavaScript is available too.

domrmx
PurposeRead and change the attached pageUse storage, network access, clipboard, notifications, menu commands, schedules, and runtime helpers
Handled byThe page agentThe background worker for privileged calls; the sandbox runtime for local helpers and subscriptions
Permission checkA fixed policy for every remixlet, including URL checks and password-field protectionApproved capabilities for privileged operations; helpers such as keep, navigation, log, and prefix need no capability
Declared in the manifestNo separate dom capabilityEach privileged capability the remixlet needs
ResultsAsynchronous element handles and plain valuesPrivileged requests return serialized data through asynchronous calls; helpers also expose properties, callbacks, and unsubscribe functions
LifetimeHandles belong to this page sessionStorage and grants belong to the remixlet across pages; callbacks and local helpers belong to this sandbox
RefusalsLogged with the page-policy reasonLogged with the missing capability or other failed check

If the thing you want is on the page, start with dom. If it needs to survive the page or use a browser service, look for an rmx capability. A dom call needs no separate approval, but it still has to pass the page policy.

The two APIs work together in rmx.keep. It registers a condition that should keep holding while a site redraws itself, with callbacks written using dom to check and repair the page. It needs no capability and is the mechanism most remixlets are built around.

The lethal trifecta#

Simon Willison calls the combination of private data, untrusted content, and external communication the lethal trifecta. A page can plant instructions that persuade an agent to read private information and send it to an attacker. Prompt rules alone are not a reliable defence.

A browser agent encounters all three. The page may show your private account data. Its comments, listings, reviews, profile bios, or hidden text may contain instructions written by strangers. The agent communicates with your provider and can use tools that act on the page. The code it writes can request network capabilities and interact with the site's own controls.

The agent and the saved remixlet create different risks. The agent can make a bad decision during a conversation. A malicious remixlet can persist across visits long after that conversation ends. Remixlet limits both by keeping page access narrow, checking actions outside the model, and asking you before a proposal widens its permissions. The following sections explain those checks and their limits.

Why the remixlet does not run in the page#

Userscripts have done this job since 2005: one JavaScript file, a metadata block at the top, a handful of GM_* functions from the manager, and the script runs in the page, or in a world beside it, with the page's document in hand. Our first build did the same. Remixlets ran in Chrome's user-scripts world, and we were grateful for the lineage. We replaced that runtime before launch, for four reasons.

The unit of trust changed. In the one-file format, power is a label. The script names the privileged functions it wants with @grant, and installing it accepts the package. That works because a human chose the script and could read it. Our author is a model that reads a live page, and the page may contain text written to steer it. So the thing that decides what a remixlet may do cannot be the script, and it cannot be the model either. It has to be a check the extension runs outside both, against a record you approved.

A world beside the page is still in the page. Chrome gives each user-scripts world its own content security policy, and ours blocked loading scripts from the network. It did not block sending. With that policy, a script could send data with fetch or an image URL while holding the page document directly. Only a prompt rule stood in its way. Moving the code into the sandbox removes that direct access and makes page operations go through extension checks.

Stored code needs a stable contract. A saved remixlet runs after the conversation that wrote it is over. If its API changes, it can fail on your page without the model there to repair it. The extension stamps an rmx.* contract version into each remixlet and marks unsupported versions as needing repair instead of running them. Before launch, a breaking change replaces the old contract and bumps that version. After launch, the API is add-only for stored code. A separate namespace lets us state that compatibility promise explicitly.

A familiar name is a promise. If remixlets opened with ==UserScript== and called GM_setValue, anyone who has used a script manager would expect their existing scripts to install. Those scripts were written for a more generous permission model, and Remixlet has no install links and no marketplace. A visibly different format and a different namespace are the promise we can keep.

What we kept is the idea, and the match-pattern syntax for where a remixlet runs. Your browser should bend websites to you and not the other way around. That is twenty years old and still right. The runtime is where it had to change to survive a new kind of author.

Shrinking the private data#

Remixlet has no backend, no account, and no telemetry. Your remixlets, their history, your conversations, and your settings live in your browser's local storage. During a conversation, the agent sends page context to the provider you connected. Running remixlets do not call the model, but their approved network capabilities and permitted page actions can send data, as described below. Provider keys stay within the panel code and are sent only to their provider. A lint rule fails the build if code outside the panel references a key at all, or if any code puts one in a URL, a log line, or an error message.

The agent sees what the job needs and not much more. Its tool for reading page state refuses cookies, local storage, session storage, IndexedDB, caches, and the credential store. Big pages arrive as a structural outline rather than raw markup. Password-field values are blanked in structured reads, but there is no general filter for sensitive page content. If the page shows your bank balance, the model sees your bank balance. Your data and privacy lists exactly what a capture contains.

A remixlet can read the page through dom, with password-field values blanked. It cannot read the site's in-memory tokens, app state, or JavaScript variables, or replace page functions to spy on them. Separate sandboxes keep remixlets from accessing each other's runtime state, though their permitted DOM changes are visible on the shared page.

Treating the page as hostile#

The agent's standing rules say page content is data, never instructions, and tell it to report attempts to steer it. A test suite feeds it pages that try. But the rule is a prompt, and Willison is right that a prompt is not a security boundary. So the design assumes the agent will sometimes be fooled, and limits what it can do even when it is fooled.

  • Nothing the agent reads or says can grant it a power. Grants come only from buttons you press in the panel, and the extension's background worker checks them outside the conversation.
  • The agent may not ask for a network host because page text suggested it. A host is justified only by what you asked for and by probes that confirmed the data lives there. That is a prompt rule too. The dialog that follows is the real gate.
  • The approval dialog is written by us, not by the model. Each power's description is fixed extension copy, and the remixlet's own stated reason never renders there. The chat has already said why the agent is asking, and where model text does appear in the panel it is stripped of control and direction-changing characters and cut at 120 characters. A manipulated model cannot write the screen you decide on.
  • The chat shows the model's replies as text. A page cannot trick the model into "showing" an image whose address carries your data to someone else's server.
  • A misled agent still has its structured page tools and can propose code. Those tools remain subject to extension checks, the code runs in the sandbox, and broader permissions require your approval. Existing permissions and permitted page actions still matter.

Narrowing the communication#

The sandbox cannot make direct network requests. Its policy blocks remote scripts, connections, form submission, and string evaluation through eval, Function, or string timers. The iframe also prevents popups and navigation out of the sandbox. The runtime disables WebRTC constructors on the real sandbox global because the browser does not reliably enforce the draft CSP directive for WebRTC. Blob scripts and workers are permitted for local code; they do not grant network access.

The browser test suite runs hostile remixlets against witness servers that record attempted traffic, including a WebRTC witness. It tests direct attempts, attempts that recover browser globals, and forbidden operations through dom.

To request network access through the extension, a remixlet declares a capability with a written reason, and you approve it before activation:

remixlet.json
{
  "capabilities": [
    "storage",
    "fetch:api.bandcamp.com"
  ],
  "capabilityRationales": {
    "storage": "Remember which albums you've hidden.",
    "fetch:api.bandcamp.com": "Look up album lengths the page doesn't show."
  }
}

The capabilities are small on purpose. storage is a private key-value store. clipboard writes text. notifications shows text. menu and schedule add a toolbar command and a timer. The network ones are the interesting ones, and each is scoped to named hosts:

  • fetch:api.example.com lets the remixlet make requests to that host as you, and nothing else. The requests go through the extension, which re-checks the host on every redirect hop and drops any authorization header when a redirect crosses to another origin. There is no "access the internet" grant.
  • network:observe:api.example.com lets the remixlet read the responses the page itself fetched from that host. The interceptor is one file we ship, registered in the page's main world. Its per-remixlet configuration arrives as data, never as code. It keeps a bounded amount of response data and passes requests and responses through without changing their contents.
  • netrules lets a remixlet block, redirect, or alter requests, from a rules file the extension checks before lending it any power. Rules are pinned to the remixlet's own sites, cannot touch security headers such as content security policy, cookies, or CORS headers, and may only redirect to an approved host over https.

Declaring a capability does not grant it. Privileged rmx.* calls travel through the offscreen host to the background worker, which checks four things: the host has attached the correct secret token for that remixlet, the page is one the remixlet's matches cover, the remixlet is not paused, and the capability is in its granted list. If any check fails, the worker refuses the call. Only the offscreen host may forward these calls, and the token never enters the sandbox or page. Remixlet A cannot borrow remixlet B's powers.

Approval is bound to a SHA-256 digest of the exact files being proposed, is single-use, and expires after ten minutes, so a yes to one proposal cannot authorize swapped-in code. The dialog shows the sites and the capabilities in plain words, not the files; the digest is what ties your yes to the code. The stored manifest is the approval record. Activation is the only way files reach the store, and it refuses any set whose capabilities you have not just approved or already held. Updates re-open the dialog whenever a new or broader capability appears, whenever the site list widens, always when a remixlet asks to run on every site, and whenever the rules file changes. Nothing widens silently.

Remote code is refused at two points. The sandbox's policy blocks scripts and imports from the network, plus eval, Function and string timers. Before that, the extension parses JavaScript files before activation and rejects a remote import(), a created script tag, an eval of anything but a literal, and an element src pointed at a remote URL. The runtime already makes the save-time check redundant as a wall. We keep it because it keeps the stored artifact honest: a remixlet that tries to fetch and run code is refused before it is saved, so the code you can read is the code that runs.

What the page agent allows, mediates, and refuses#

The sandbox has no door to the network, so the question moves to the page agent: what can a remixlet do to the page, and could any of it carry data out?

  • Reads are bounded by the API's supported operations and size limits. The value of a password field reads back empty in structured results: the value, the attribute, the surrounding HTML, and the keys of an event typed into it. A read can travel through the image and link rule below, so the one field whose whole purpose is a secret stays in the page.
  • Creating layout and text elements is allowed, with attributes vetted. Creating a form, iframe, script, style, link, meta, object, embed, video, or audio element is refused.
  • Setting on*, src, href, action, srcdoc, style and the other attributes that load or run something is refused. Two are mediated instead: href on a link and src on an image. The URL is resolved the way the browser would resolve it, against the document's base, and the result must be on the page's own origin, on one of the remixlet's own sites, on a host an approved fetch: capability names, or be a URL the page already loads exactly as written, or a data: image. Never javascript: or blob:. What lands on the element is that absolute URL, not the string the remixlet passed, so a page that moves its base afterwards cannot redirect the load.
  • An inline style containing url(, @import or their relatives is refused. HTML handed to the page is parsed in an inert document first, with refused tags dropped, refused attributes stripped, and URLs vetted by the same rule.
  • An attribute or class added to one of the page's own elements must carry the remixlet's prefix, so every mark on a page names the remixlet that wrote it. Leftovers from a remixlet that no longer exists are listed, not guessed at.
  • Clicks check the controls they activate, including controls reached through a label. Link and form destinations must pass the URL policy described above. Controls with formaction are refused. Permitted clicks can still trigger the site's own actions.
  • dom has no direct navigation, window.open, or form-submission verb. Allowed link and submit-control clicks can still navigate or submit through the page.

Every refusal is written to the remixlet's log with its reason, so the agent fixes the code instead of guessing.

Inspecting, changing, and stopping a remixlet#

A remixlet is plain, unminified code, and each one keeps its own git history. Every change is a commit you can open as a colored diff, and rolling back to any version is one click. The manifest and its reasons are always visible. If something misbehaves you can toggle the remixlet off, pause every remixlet on a site at once, archive it, or delete it forever. Deleting it destroys every grant it held, because the grant record is the manifest. Pause is enforced in the worker, not only at injection: a paused remixlet's calls are rejected even if its sandbox is still running in an open tab, and its schedules and menu items go dead with it. Disabling, deleting, rolling back, pausing and resuming reload matching tabs to clear page changes. The host also tears down removed or replaced sandboxes and their page listeners and observers, so revocation does not depend solely on a successful reload. Reloading cannot undo a request or site action that already happened.

Some things we chose not to build. There is no marketplace and no install link, so nobody can publish a remixlet into your browser. There is no export to a standalone script, because an exported script would carry its powers outside these walls with no one checking calls at the door. A capability is only real because the extension stands behind it.

Where you still come in#

Everything above narrows the trifecta. It does not dissolve it. The three legs still meet in your browser, and some decisions are only yours to make.

  • Choose the pages. Don't point the agent at a page you would not paste into a chat with your provider. Your bank, a health portal, an admin console. The extension does nothing on a site until you ask it to.
  • Read the dialog, not just the button. "Send requests to" a host means the remixlet makes requests from your browser, signed in as you. "Runs on every site you visit" means every site. If a request names a host you never mentioned, say no and ask the agent why it wants it.
  • Skim the code when the stakes are up. A remixlet that should only hide elements has no business calling rmx.fetch, setting an image's src, or typing into a form. The diff view makes those easy to spot.
  • Listen when the agent says something odd. It is told to report manipulation. When it does, that page is hostile to agents, and a remixlet built on it deserves a second look.
  • Keep the kill switches in mind. Toggle off, pause the site, archive, delete. They are all one click from the toolbar or the manager.

Where to go next#