Greasemonkey and Userscripts: Custom Browser Automation Without Publishing an Extension
Userscripts are JavaScript files you install in a browser extension like Greasemonkey or Tampermonkey, which then inject them into specified pages. You write a script once; it runs every time you visit the matching URLs. No review process, no packaging, no publication — a user-run script that customizes or automates any page you visit. For personal productivity hacks, removing annoyances from specific sites, or automating repetitive tasks, userscripts are a fast and direct tool.
Greasemonkey, Tampermonkey, Violentmonkey: Which to Use
Greasemonkey is the original, created for Firefox in 2004. The current version (4.x) runs scripts in a true sandboxed environment separate from the page. This is more secure but breaks older scripts that expected the sandboxed context to share scope with the page. Greasemonkey 4 removed several GM_* APIs that scripts depended on.
Tampermonkey is the most widely used userscript manager today. Available for Chrome, Firefox, Edge, Safari, and Opera. Less sandboxed than Greasemonkey 4 — scripts run in a context with access to page globals, which makes legacy scripts more compatible. Feature-rich: synchronization of scripts via cloud storage, script update notifications, a built-in editor.
Violentmonkey is open source, available on Chrome and Firefox, and has become a popular choice for privacy-conscious users who prefer auditable, actively-maintained open-source software. It’s broadly compatible with Tampermonkey’s script API. Available at violentmonkey.github.io.
For new users, Tampermonkey has the best documentation and widest script compatibility. For privacy-focused users who want to audit the extension itself, Violentmonkey is the better choice. Greasemonkey on Firefox is fine for simple modern scripts but avoid it for complex scripts that use old GM_* APIs.
Userscript Anatomy
Every userscript starts with a metadata block:
// ==UserScript==
// @name Remove Cookie Banners
// @namespace https://example.com/
// @version 1.0
// @description Remove cookie consent banners automatically
// @author YourName
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// Your code here
})();
Key metadata fields:
@match— URL pattern(s) the script runs on. Use*://*/*for all pages, or specific patterns like*://news.example.com/*.@include/@exclude— older alternatives to@match;@matchis preferred.@grant— declare which GM_* APIs your script uses. Use@grant noneto run in an unsandboxed context with full access to page globals but no GM_* APIs.@run-at— when to run:document-start(before DOM loads),document-end(after DOM),document-idle(after page load completes).@require— load an external script before yours runs. Used to load jQuery or other libraries.
The Tampermonkey documentation covers all metadata keys.
Practical Examples
Remove an Annoying Element
// ==UserScript==
// @name Hide Sidebar Ads
// @match *://targetsite.com/*
// @run-at document-end
// @grant none
// ==/UserScript==
(function() {
const sidebar = document.querySelector('.ad-sidebar');
if (sidebar) sidebar.remove();
// For elements that load after the initial page, use MutationObserver
const observer = new MutationObserver(() => {
const ad = document.querySelector('.late-loading-ad');
if (ad) ad.remove();
});
observer.observe(document.body, { childList: true, subtree: true });
})();
Add Keyboard Shortcuts
// ==UserScript==
// @name Custom Keyboard Shortcuts
// @match *://app.example.com/*
// @grant none
// ==/UserScript==
document.addEventListener('keydown', event => {
if (event.ctrlKey && event.key === 'j') {
document.querySelector('[data-action="next"]')?.click();
}
});
Store Persistent Data with GM_setValue
// ==UserScript==
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
// Store a setting
await GM.setValue('hidePromotedContent', true);
// Read it back (on next page load)
const hide = await GM.getValue('hidePromotedContent', false);
if (hide) { /* ... */ }
GM.setValue / GM.getValue (the modern Promise-based API) persist data across page loads and browser restarts. They’re stored by the userscript manager, not in page localStorage.
Where to Find Userscripts
Greasy Fork (greasyfork.org) is the primary community repository. Scripts are user-submitted and reviewed for obvious malicious behavior, but review is not comprehensive. Read script descriptions, check ratings and install counts, and look at the script source before installing.
OpenUserJS (openuserjs.org) is an alternative repository with similar content.
For site-specific scripts, searching “site-name userscript” often surfaces forum posts and GitHub repositories.
Security Considerations
Userscripts run with the same access as the page’s JavaScript and, with declared @grant permissions, also have access to GM_* APIs including cross-origin requests (GM_xmlhttpRequest), storage, and clipboard. A malicious userscript can:
- Exfiltrate your form input, including passwords
- Make requests to arbitrary domains (with
@connectandGM_xmlhttpRequest) - Read cookies that are accessible to the page
- Modify page behavior to introduce new vulnerabilities
Install only scripts you’ve read. Or at minimum, scripts from trusted authors on well-established repositories with high install counts and recent activity. Even then, read the code if you’re installing it on pages where you handle sensitive data.
The @require metadata key is a common vector — a script can require an external library from an arbitrary URL. If that URL changes (compromised CDN, author control lost), the new content executes in your browser. Scripts that use @require with versioned CDN URLs or bundled code are safer than those requiring mutable URLs.
GM_xmlhttpRequest: Cross-Origin Requests
A capability userscripts have that normal page JavaScript doesn’t: GM_xmlhttpRequest (or the modern GM.xmlHttpRequest) can make requests to domains other than the current page, bypassing CORS:
GM.xmlHttpRequest({
method: 'GET',
url: 'https://api.other-domain.com/data',
onload: response => console.log(response.responseText)
});
Declare the allowed domains in @connect:
// @connect api.other-domain.com
This is powerful for pulling in external data, but also the capability that makes malicious userscripts dangerous — they can exfiltrate data to any @connect-declared domain.
Userscripts vs Extensions
Userscripts are simpler to write (no manifest, no background worker, no permissions flow) and faster to iterate on. They’re appropriate for personal customizations, one-off automation, and site-specific tweaks you don’t need to share or maintain long-term.
Extensions are appropriate for: software you distribute to others, features that require browser APIs unavailable to userscripts (chrome.tabs, chrome.webRequest, chrome.storage.sync), more complex UIs (popup pages, options pages), or anything where you need the Google/Mozilla review process as a credential.
The line between them blurs for personal productivity tooling. Some power users maintain small libraries of userscripts for their own use; the barrier to entry is lower than extension development, and the iteration cycle is faster.
FAQ
Do userscripts work on all websites?
They work on any site where the URL matches your @match pattern and the site’s Content Security Policy allows script injection. Some sites use strict CSPs that may prevent userscripts from executing or restrict what they can do. CSP blocking of userscripts is uncommon for consumer sites but more frequent on security-sensitive applications.
Can a userscript break a site permanently? A userscript runs per-page-load; it can’t modify server state. It can modify your local storage for a site, set cookies, or make API requests with your credentials if the site doesn’t use CSRF protection. Disabling the userscript and clearing site data restores the site to normal.
Are there userscript managers for iOS/Android? Mobile browsers have restricted extension APIs that limit full userscript support. On iOS, Userscripts (an app by Justin Wasack) supports scripts in Safari via the iOS extension API. On Android, Firefox supports Tampermonkey and Violentmonkey directly.
What’s the difference between GM_ and GM. APIs?**
The older GM_function() style (synchronous callbacks) is the legacy API. The modern GM.function() style returns Promises. Tampermonkey and Violentmonkey support both. New scripts should use the Promise-based API; some old scripts still use the legacy style, which is why it hasn’t been removed.