What Is a Userscript Manager and How It Works

A userscript manager is a browser extension that injects small pieces of JavaScript into pages you visit, based on rules you or the script author define. Understanding how it works helps explain why scripts from the HatScripts /userscripts hub behave differently from a browser extension or a bookmarklet.
The extension model
A userscript manager is itself a regular browser extension, built on the same content script and background script APIs available to any extension developer. Its job is to act as a host for other, smaller scripts. It keeps a list of installed userscripts, watches page navigation events, and decides for each page load which scripts should run.
This is different from a script author needing to publish their own extension. Anyone can write a userscript as a plain text file, and it will run under the manager’s existing permissions without needing separate browser store review.
Matching rules
Each script declares @match or @include patterns in its metadata block, such as @match https://example.com/*. When you navigate to a page, the manager checks the URL against every installed script’s patterns and only injects the scripts that match. @exclude patterns can further narrow this down for specific paths.
@match uses a restricted pattern syntax closer to the one used by browser extension manifests, requiring a scheme, a host and a path, while @include historically allowed full regular expressions. Because @match is stricter, it is also easier for the manager and the user to reason about exactly what a script can reach, which is why most managers recommend it over @include for new scripts.
Injection timing
Scripts can request a specific point at which to run using the @run-at directive.
- document-start: runs before the page’s own scripts and before the DOM is built, useful for patching functions early
- document-end: runs after the DOM is parsed but before images and other resources finish loading
- document-idle: the default, runs after the page has mostly finished loading, similar to a DOMContentLoaded-adjacent point
- context-menu: runs only when triggered from a browser context menu entry, not automatically
Sandboxing and unsafeWindow
For security, userscript code usually runs in an isolated JavaScript context separate from the page’s own scripts, similar to how content scripts work in regular extensions. This means a userscript cannot directly read variables the page defines, and the page cannot read variables the userscript defines.
When a script genuinely needs to interact with the page’s own JavaScript objects, most managers expose an unsafeWindow reference that breaks this isolation intentionally. This should be used sparingly, since it removes a layer of protection between the script and whatever the page itself is running.
A practical way to think about this isolation is that the DOM itself is shared, since it is a single tree the browser renders, but the JavaScript variables and functions each side defines are not. A script can freely read and modify visible page elements through document.querySelector and similar calls without touching unsafeWindow at all, and most scripts never need it.
The metadata block
Every userscript starts with a comment block between // ==UserScript== and // ==/UserScript== containing fields such as @name, @version, @match, @grant and @updateURL. The manager parses this block to populate its dashboard and to decide which privileged APIs, like GM_setValue, the script is allowed to call.
The block is plain text and follows a simple one-field-per-line convention, which is why it can be parsed identically by different managers from different vendors. This shared convention is a big part of why the same .user.js file generally installs and runs the same way whether you use Tampermonkey, Violentmonkey or another compatible manager.
Storage between page loads
Because userscripts do not share the page’s own localStorage by default, managers provide their own storage layer through GM_setValue and GM_getValue, or the promise-based GM.setValue and GM.getValue. This storage persists across page loads and browser restarts and is scoped separately for each script.
How this differs from extensions and bookmarklets
A full browser extension needs its own manifest, its own store listing and review, and its own set of declared permissions. A userscript skips all of that by running inside an already-installed manager, which makes distribution far simpler, as seen with the plain text files linked from the HatScripts /userscripts hub.
A bookmarklet is a single URL saved as a bookmark containing JavaScript that runs once when clicked. It has no persistence, no automatic matching against pages, and no access to privileged GM_ APIs. A userscript, by contrast, runs automatically on every matching page and can maintain state across visits.
Performance considerations
Injecting a script costs some amount of time on every matching page load, even if the cost is usually small. A script that runs at document-start and does expensive DOM traversal before the page has rendered anything can visibly slow down first paint, while the same logic run at document-idle usually goes unnoticed. Choosing the least aggressive @run-at value that still lets the script do its job is a simple way to keep overhead low.
Scripts that poll the DOM with setInterval to wait for an element to appear are a common source of unnecessary CPU use. A MutationObserver targeting a specific container is almost always both faster and lighter than a polling loop, and well-written scripts tend to prefer it.
A worked example of the injection pipeline
To make the pieces above concrete, consider a script with @match https://example.com/*, @run-at document-idle and @grant GM_setValue that adds a small counter badge to the page. When you navigate to example.com, the manager’s background logic matches the URL against the script’s @match pattern, confirms the script is enabled, and waits for the point described by @run-at before injecting the code into an isolated context for that page.
Once running, the script can call document.querySelector to find a place to insert the badge, since the DOM itself is shared, and can call GM_setValue to remember the counter’s value the next time the page loads. None of this requires the page’s own scripts to know the userscript exists at all, which is the isolation described above working as intended.
Update checks and version handling
A userscript manager treats @version purely as a comparison value used to decide whether a newer copy is available at @updateURL, not as anything that changes how the script itself runs. Bumping the version and pushing a new file is the entire update mechanism. There is no separate build step or packaging format involved, which is part of why userscripts remain simple to maintain compared to a full browser extension.
Mobile browser behaviour
The injection model described here depends on the underlying browser exposing extension APIs to the manager, which is why mobile support is inconsistent. Kiwi Browser and Firefox for Android expose enough of the standard extension surface for a manager to work the same way it does on desktop. Default mobile Chrome and Safari do not expose this to arbitrary extensions, which is why Safari instead relies on Apple’s separate Safari Web Extension model rather than the Chromium-style approach described throughout this piece.
Common misconceptions worth clearing up
A few points about how managers work are frequently misunderstood, so it is worth stating them plainly.
- A userscript is not a browser extension in its own right, and it cannot be installed or run without a manager present
- Isolation between a script and the page does not mean the script cannot see or change the page. It can, through the shared DOM, just not through shared JavaScript variables
- @match does not guarantee a script only affects the specific elements it targets. It only limits which pages the script runs on at all
- A higher @grant list does not make a script slower by itself. Performance depends on what the script actually does once it runs, not on which APIs it is permitted to call
Frequently asked questions
Can a userscript manager run scripts without any manager installed? No, the manager is required, since it is what parses the metadata block and performs the injection. A .user.js file by itself is inert.
Is unsafeWindow always available? It depends on the manager and whether the script declared @grant unsafeWindow. Some managers restrict it further under Manifest V3 due to stricter isolated-world rules.
Do userscripts run on browser internal pages like settings screens? No, browsers block extensions, including userscript managers, from injecting into their own internal pages for security reasons, regardless of what @match patterns a script declares.
Questions about the tools in this guide
Short answers about the hubs this article touches, each linking straight to the tool.