RemixletDocs

How it works

Security architecture

How the agent builds a remixlet, how the sandbox runs it, what the extension checks on every call, and where those protections stop.

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

This matters because the agent reads websites where you are signed in, and those websites can contain instructions planted by someone else. This page starts with where the work happens, then covers the threat, the checks, the known gaps, and the decisions that stay with you.

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 or moves to another site, the agent stops. Switching tabs does not move the conversation to another page.

Its tools are fixed code in the extension. They capture the page, outline its structure, query elements, inspect styles, read the data the page loaded, click elements, open other pages on the same site, write remixlet files, and read a remixlet's log. The model fills in each tool's parameters, and those cross into the page as data, never as a script. The agent has no tool that runs code it wrote on the page. Code it writes runs only as a remixlet, in the sandbox.

When the conversation ends, the agent stops. The saved remixlet keeps running without another call to the model. Data and privacy lists what the agent reads and what it can do on the site.

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 closing the tab tears it 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 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 replaced with errors that name the supported API, and window and location have limited read-only replacements. The replacements help the agent write correct code. The boundary itself is the sandbox and the extension's message checks.

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 into the page's isolated world. It holds the real document and checks every request to read or change it. The background worker handles privileged requests against the stored approval record. The offscreen host passes messages between them and the sandbox, and attaches a secret token when it forwards a privileged request. Neither the remixlet nor the page ever holds that token.

The remixlet's stylesheet takes a different route. The extension inserts style.css into the page itself, after checking what it loads. See Stylesheets.

The two APIs#

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

domrmx
PurposeRead and change the page the sandbox is attached toStorage, network requests, clipboard, notifications, menu commands, schedules, and runtime helpers
Handled byThe page agentThe background worker for privileged calls; the sandbox runtime for helpers and subscriptions
Permission checkA fixed policy for every remixlet, including URL checks and password-field protectionApproved permissions for privileged calls; helpers such as keep, navigation, log, and prefix need none
Declared in the manifestNothing to declareEach permission the remixlet needs
ResultsAsynchronous element handles and plain valuesPlain data from asynchronous calls; helpers also expose properties, callbacks, and unsubscribe functions
LifetimeHandles belong to this page loadStorage and permissions belong to the remixlet across pages; callbacks belong to this sandbox
RefusalsLogged with the page-policy reasonLogged with the missing permission or other failed check

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

The two meet in rmx.keep. It registers a condition that should stay true while the site redraws itself, with callbacks written in dom that check and repair the page. It needs no permission, and most remixlets are built around it.

The lethal trifecta#

Simon Willison calls the combination of private data, untrusted content, and a way to send data out the lethal trifecta. When an agent has all three, a page can plant instructions that persuade it to read private information and send it to an attacker. Rules in the prompt are not a reliable defence.

A browser agent has 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 talks to your provider, acts on the page, and writes code that can ask for network permissions.

The agent and the saved remixlet carry different risks. The agent can make a bad decision during one conversation. A malicious remixlet can keep running on every visit long after that conversation ends. Remixlet limits both: page access is narrow, actions are checked outside the model, and a proposal that widens its permissions has to ask you. The sections below explain those checks and where they stop.

Why the remixlet does not run in the page#

User scripts have done this job since 2005: one JavaScript file, a metadata block at the top, a handful of GM_* functions from the script 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, running remixlets in Chrome's user-scripts world. We replaced that runtime before launch, for four reasons.

The author changed. In the one-file user-script format, power is a label. The script lists the privileged functions it wants with @grant, and installing it accepts the whole package. That works because a person 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 what a remixlet may do cannot be decided by the script, or by the model. 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 could not stop sending. A script holding the page's document could send what it read with fetch or an image address, and only a rule in the prompt stood in its way. In the sandbox the code has neither the document nor the network, and every page operation goes through the page agent.

Stored code needs a stable contract. A saved remixlet runs long after the conversation that wrote it, with no model there to repair it if the API changes under it. The extension stamps an API version into each remixlet and does not run versions it no longer supports. Since the first public release the API only grows: nothing stored code can call is renamed, removed, or changed. A separate namespace lets us make that promise.

A familiar name is a promise too. 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. A visibly different format is 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, not the other way round. That idea is twenty years old and still right; the runtime had to change for a new kind of author.

Keeping private data small#

Remixlet has no backend, no account, and no telemetry. Your remixlets, conversations, and settings live in your browser. During a conversation the agent sends page content to the provider you connected, and it can read far more than the visible page: the data the site's scripts load, the values in its JavaScript variables, and the current contents of form fields other than passwords. There is no filter for sensitive content. Data and privacy lists exactly what is read, sent, and stored.

What the agent cannot read is enforced in code. Its tool for reading page state refuses cookies, local storage, session storage, IndexedDB, caches, and the credential store. Password fields read as empty. Big pages arrive as an outline rather than raw markup.

A running remixlet sees less than the agent. It reads the page through dom, with password values blanked. It cannot read the site's JavaScript variables or in-memory tokens, or replace page functions to watch them. Separate sandboxes keep remixlets from reading each other's state, though each can see the others' changes to the shared page.

Provider API keys stay in the panel and are sent only to their provider. A lint check fails if code in the background worker or the page bridge refers to a key, or if any code puts a key or sign-in token into a URL, a log line, or an error message. A ChatGPT sign-in is the exception: the background worker keeps and renews its tokens, and sends them only to OpenAI.

Treating the page as hostile#

The agent's instructions say page content is data, never instructions, and tell it to report attempts to steer it. But that 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 a fooled agent can do.

  • Nothing the agent reads or says can grant it a permission. Permissions come only from buttons you press in the panel, and the 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 checks that confirmed the data lives there. That is a prompt rule too. The permission card that follows is the real gate.
  • The permission card and dialog are written by us. Each permission's description is fixed extension text, and the reason the model wrote for it does not appear there. A manipulated model cannot write the text you decide on. The one exception is restoring an older version in the manager, whose confirmation lists that version's permission names with the reasons stored in it.
  • The chat shows the model's replies as formatted text with images removed, so a page cannot trick the model into loading an image whose address carries your data. Links in replies stay clickable. Check where a link goes before you follow it.
  • A test suite runs the agent against a scripted model that obeys instructions planted in a page, and checks that the fooled agent stays contained. It tests the containment, not how often a real model is fooled.

What a fooled agent can still do is limited to its tools and your approvals. It can propose code, which runs in the sandbox and cannot widen its permissions without asking you. It can also click, submit forms, and repeat the page's requests on the site you are working on, and any permissions a remixlet already holds still apply.

Narrowing the communication#

The sandbox cannot make network requests. Its policy blocks remote scripts, connections, form submission, and eval, Function, and string timers. The iframe cannot open popups or navigate. The runtime also removes the WebRTC constructors, because browsers do not reliably enforce the content security policy rule for WebRTC. Scripts and workers made from blob: addresses are allowed for local code; they get no network access either.

The browser test suite runs hostile remixlets against witness servers that record any traffic that gets through, including a WebRTC witness. It tries direct requests, tricks to recover the browser's real globals, and forbidden operations through dom.

Permissions#

To use anything beyond the page, a remixlet declares a permission with a written reason, and you approve it. The full list is in Anatomy of a remixlet. The small ones are storage, clipboard (write only), notifications (text only), and menu. schedule runs no remixlet code later: the worker runs one of three fixed actions, which are to show a text notification, open or focus a tab on one of the remixlet's own pages, or queue a message for the remixlet's next page load.

The network permissions are the ones that matter, and each is scoped to named hosts. There is no "access the internet" permission.

  • fetch:api.example.com lets the remixlet send requests to that host, signed in as you. The extension sends them, checks every redirect against the approved hosts, and drops any authorization header when a redirect crosses to another origin. Requests may use http as well as https.
  • network:observe:api.example.com lets the remixlet read the responses the page itself receives from that host. The recorder is one file we ship, registered in the page's main world, with its per-remixlet settings passed as data, never as code. It keeps a bounded amount and does not change requests or responses.
  • netrules lets a remixlet block, redirect, or change the requests web pages make, with declarative rules. No rule acts on a request Remixlet itself sends, such as your sign-in or a message to your AI provider. A remixlet that runs on some sites has rules that act only on requests their pages make; one that runs on every site has rules that act on every page's requests. A redirect applies only to GET requests, so a posted form or any other request with a body is never sent to another address. It must go to an https address on the remixlet's own sites, or, for a remixlet that runs on every site, on the same site as the hosts the rule names. No rule may touch security headers: content security policy, cookies, CORS, authorization, HSTS, frame options, and a few others. The extension refuses a redirect that breaks these limits when the agent saves it, and checks every rule again each time it installs them. A rule saved before Remixlet 1.0.2 that breaks a limit is left out, the remixlet's other rules still apply, and a line in its log says which rule and why.

Stylesheets#

A remixlet's style.css does not run in the sandbox. The extension inserts it into the page, so a url() in it is a request the page makes. A rule keyed on page content, such as input[value^="a"] { background: url(…) }, even chooses which address is requested. So the extension checks what every stylesheet loads, reading it the way Chrome's CSS parser does, so comments, strings, and escapes cannot hide a load.

A stylesheet may load only a full https:// or http:// address on a host its matches cover or a fetch: permission names. A network:observe: permission allows no loads. data: and relative addresses, @import, image-set(), and the other ways CSS can load or run something are refused. A remixlet that runs on every site can load from any host, as its images can; the dialog showed that scope as every site when you approved it.

The check runs when the agent saves the code, again at activation, and every time the stylesheet is inserted into a page. A stylesheet that needs another host has to come with a fetch: permission for it, which you approve as usual. A remixlet saved before this rule arrived in Remixlet 1.0.2 keeps running. When its stylesheet is inserted, the parts that load from elsewhere are left out, and a line in its log says which. CSS the script writes through dom is stricter still: it may not contain url() at all.

How approval works#

You approve permissions in two places.

  1. When the agent finds that a change needs a permission, it asks in the chat with a "Remixlet needs permission" card. Each row is fixed text, such as "Send requests to api.example.com", with "Allow" and "Not now". This happens before the code is written.
  2. When the agent saves the code, the extension compares the new manifest with what you allowed and with the version already running. If it asks for nothing beyond that, it activates without another question. If it would run on more sites, on every site, on sites other than the chat's, or with changed network rules, or if it would move a remixlet that also runs on other sites onto just the chat's site with the permissions it already has, a dialog asks first, with "Allow and apply" and "Don't allow". Declining keeps the previous version.

Each activation approval is bound to a SHA-256 digest of the exact files, can be used once, and expires after ten minutes, so an approval for one set of files cannot activate another. Pending approvals are dropped if the tab leaves the site. There is no separate grant record: activation is the only way files reach the store, and it refuses any set whose permissions you have not allowed, so the stored manifest is the record of what you approved. Delete the remixlet and its permissions go with it.

Your "Allow" approves a permission, not code you have read. The digest guarantees that what activates is exactly what was checked against your approval, not that you looked at it.

Checking every call#

Declaring a permission does not grant it. A privileged rmx call travels through the offscreen host to the background worker, which checks four things: the token belongs to that remixlet, the page is one its matches cover, neither the remixlet nor the site is paused, and the permission is in its manifest and approved. If any check fails, the worker refuses the call. The one exception is rmx.log, which skips the page and pause checks so a paused remixlet can still record why it failed. Only the offscreen host can forward these calls, and the token never enters the sandbox or the page, so one remixlet cannot use another's permissions.

Refusing remote code#

The sandbox policy blocks scripts and import() from the network, and eval, Function, and string timers. Separately, when the agent saves a script, the extension parses it and rejects a remote import(), a created script element, an eval of anything but a literal, and an element src set to a remote address. That save-time check only recognises literal addresses, so it is not what stops remote code; the sandbox policy is. We keep it because a remixlet that tries to load code is then refused before it is saved, and the code you can read is the code that runs.

What the page agent allows, checks, and refuses#

The sandbox has no network, so the remaining question is what a remixlet can do to the page, and whether any of it could carry data out.

  • Reads are limited to the API's operations and size caps. A password field reads back empty: its value, its attribute, the surrounding HTML, and the keys of an event typed into it. A copy made with clone starts empty, and the field's type, autocomplete, pattern, minlength and maxlength cannot be changed, so a remixlet cannot turn it into a plain text field. A selector cannot test the value either: a selector such as [value^="a"] is refused while the page has a password field, so the value cannot be read a character at a time, and a stylesheet rule that tests the value and loads an address is refused too. A field counts as a password field when its type is password or its autocomplete names a current or new password. Otherwise a read could travel out through the link and image rule below.
  • A remixlet can create layout, text, form-control, image, and simple SVG elements, from a fixed list. It cannot create a form, frame, script, style, link, meta, object, embed, video, or audio element.
  • Attributes that load or run something are refused: on* handlers, src, action, srcdoc, style, and others. Two are checked instead: href on a link and src on an image. The address is resolved the way the browser would resolve it, 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: permission names, an address the page already loads exactly as written, or a data: image. Never javascript: or blob:. The element gets the resolved address, not the string the remixlet passed, so a page that changes its base address later cannot redirect the load.
  • An inline style or a dom.addStyle sheet containing url(, @import, or their relatives is refused. HTML handed to the page is parsed in an inert document first, with refused elements dropped, refused attributes stripped, and addresses checked by the same rule.
  • An attribute or class added to one of the page's own elements must carry the remixlet's mark, so every mark on a page names the remixlet that wrote it.
  • A click is checked against everything it would activate, including a control reached through a label and the form a button submits. Link and form destinations must pass the address rule. Clicking anything with formaction is refused. A permitted click can still trigger the site's own actions.
  • dom has no way to navigate, open a window, or submit a form directly. A permitted click on a link or submit button can still navigate or submit through the page.

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

Inspecting, changing, and stopping a remixlet#

A remixlet is plain code, and each one keeps its own version history. In the manager, History shows each version's full code and what it changed, and "Restore" brings any version back. You can switch a remixlet off in the toolbar popup or the manager, pause every remixlet on a site from the popup, and archive or delete it from the manager. Deleting a remixlet removes every permission it held, because its manifest is the record of them.

Pause is enforced in the worker, not only when a page loads. A paused remixlet's calls are refused even if its sandbox is still running in an open tab, and its schedules and menu commands stop working. Pausing, resuming, deleting, restoring a version, and switching off from the manager reload the matching tabs to clear the remixlet's changes; switching off from the popup reloads the current tab. The offscreen host also tears down removed or replaced sandboxes with their page listeners and observers, so switching a remixlet off does not depend only on a successful reload. A reload cannot undo a request or a site action that already happened.

Some things we chose not to build. There is no marketplace and no install link, so nobody can put a remixlet into your browser. There is no export to a standalone script, because an exported script would take its permissions somewhere nothing checks its calls.

Known gaps#

These are the places where the protections above stop. We would rather list them than have you find them.

  • A link or image can point at any address the page already loads. That includes addresses on other hosts. A remixlet cannot invent such an address, but which of them it picks, and when, can carry information to that host.
  • You approve permissions before the code exists. The digest ties an activation to exact files, but nobody has to read those files first. For permissions that matter, read the code in History.
  • The dialog does not list a remixlet's network rules. "Change what pages load" covers every rule in its rules.json, including where a redirect goes. Read that file in History to see them.
  • The agent acts on the site as you. While it works it can click, submit forms, open pages on the same site, and repeat the page's GET requests with your cookies. A fooled agent can do those things on the site you are working on.
  • rmx.fetch allows plain http. Requests to an approved host are unencrypted if the remixlet asks for an http address.
  • A site's own "show password" button can expose a password. It turns the field into a plain text field. If the site also marks the field with autocomplete="current-password" or "new-password", as most login forms do, it still reads as empty. Without that marker, it reads like any other text field.
  • The save-time code check sees only literals. It catches import("https://…"), not an address built at run time. The sandbox policy is what blocks remote code.

Where you still come in#

Everything above narrows the trifecta. It does not remove it. Some decisions are only yours.

  • 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. Remixlet does nothing on a site until you ask it to.
  • Read the card, not just the button. "Send requests to" a host means the remixlet sends requests from your browser, signed in as you. "Run on every site?" means every site. If a permission names a host you never mentioned, choose "Not now" and ask the agent why it wants it.
  • Skim the code when the stakes are high. A remixlet that should only hide elements has no business calling rmx.fetch, setting an image's src, or typing into a form. History › "Changes" shows exactly what each version changed.
  • Listen when the agent says something odd. It is told to report attempts to manipulate it. When it does, that page is hostile to agents, and a remixlet built on it deserves a second look.
  • Check links before you click them, in the chat as on any web page.
  • Know how to stop it. Switch it off or pause the site from the toolbar popup; archive or delete it from the manager.

Where to go next#