Docs / Userscripts guide
Userscripts
Noodle runs userscripts written for Tampermonkey and Violentmonkey without changes. Where the two managers differ, Noodle follows Violentmonkey and accepts Tampermonkey's spelling too. This page covers the header, the GM API, what Noodle adds, and how to write and debug a script.
The examples live in examples/userscripts/. None of them are installed for you.
starter.user.jsshows the reading time of an article. The walkthrough below uses it.hide-youtube-shorts.user.jshides the Shorts shelves on YouTube withGM_addStyle.youtube-transcript.user.jsadds aget_transcripttool that agents connected to Noodle can call.
Installing a script
Settings > Userscripts has three ways in: New or paste (an editor), From file, and an address box with Get script. Following a link to a .user.js file (a Greasy Fork install button, a raw GitHub file) does the same as Get script. Each way shows an install card first, with the author, the sites, what the script asks for, the hosts it may contact, the files it pulls in, its license and any warnings. A script whose @name and @namespace match an installed one is offered as an update to it.
Import backup takes a Violentmonkey export zip, a Tampermonkey zip or a Tampermonkey JSON backup, with stored values and on or off states. Export all writes a zip in Violentmonkey's format, so you can move back.
Walkthrough: the starter script
Open examples/userscripts/starter.user.js next to this page. It starts with the header:
// ==UserScript==
// @name Starter: reading time
// @namespace noodle.examples
// @version 1.0
// @description Shows how long the article on the page takes to read. Copy it and make it yours.
// @match https://en.wikipedia.org/wiki/*
// @match https://developer.mozilla.org/*
// @run-at document-idle
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @noframes
// ==/UserScript==
@name and @namespace together identify the script; change both when you copy it, or Noodle will treat your copy as an update to the original. The two @match lines are the sites it runs on. @run-at document-idle runs it after the page has loaded, so the article text is there to count. Each @grant line unlocks one GM function.
The body counts the words in the article, adds a style with GM_addStyle and appends a small badge:
const WORDS_PER_MINUTE = GM_getValue('wpm', 230)
const article = document.querySelector('main, article, #content') ?? document.body
const words = article.innerText.split(/\s+/).filter(Boolean).length
const minutes = Math.max(1, Math.round(words / WORDS_PER_MINUTE))
GM_getValue('wpm', 230) reads a stored value, or 230 the first time. The menu command at the end lets you change it:
GM_registerMenuCommand('Set reading speed', () => {
const answer = prompt('Words per minute', String(WORDS_PER_MINUTE))
const wpm = Number(answer)
if (answer !== null && wpm >= 50 && wpm <= 1000) {
GM_setValue('wpm', wpm)
location.reload()
}
})
The command shows under the userscripts button in the top bar, in the command palette as "Script: Set reading speed", and under Userscripts in the page's context menu. The value is saved per script in the encrypted profile and is there after a restart.
To try it: Settings > Userscripts > From file, pick starter.user.js, Install, then open any Wikipedia article. The badge sits in the bottom right corner.
Working on a script in your own editor
Settings > Userscripts > More on a script row > Track local file. Pick the script's file on disk. From then on Noodle reads the file whenever it changes and saves it over the installed copy, the same way the built-in editor's Save does. Reload the page to run the new version. The row says which file it follows, with a Stop tracking link.
A few details:
- Noodle watches the file's folder, so editors that save by writing a new file and renaming it over the old one work too.
- If the saved file has no valid header, or a
@requirefails to download, the installed copy stays as it was and the row shows the error, starting with "Local file:". The next good save clears it. - An edit that asks for new sites, grants,
@connecthosts,@requireor@resourcefiles waits for review, as a script update does. The row says the tracked file asks for more; Review shows the change, Skip keeps the installed copy. - The file has to keep the script's
@nameand@namespace. A file with other ones is not saved over the script, and picking such a file doesn't start tracking. - If the file or its folder goes away, the row says so with a "Local file:" error.
- Edit is off while a script is tracked, because the next change to the file would replace an edit made in Noodle. Edit the file, or Stop tracking first.
- Only a file you pick in the open dialog can be tracked. Neither pages nor scripts can set or change the path.
- Tracking survives a restart, and an edit made while Noodle was closed is read when it starts.
- Update checks skip a tracked script, so a newer version on Greasy Fork won't overwrite your work.
Header
Noodle reads:
@nameand@description(with@name:xxand@description:xxtranslations),@namespace,@version,@author,@icon(@iconURL,@icon64),@homepage(@homepageURL,@website,@source),@supportURL,@license.@match(Chrome match patterns),@includeand@exclude(globs with*and.tld, or a/regex/),@exclude-match.@run-at:document-start,document-body,document-end(the default),document-idle, orcontext-menu(the script runs when you pick it under Userscripts in the page's context menu).@grant,@inject-into(page,content,auto),@unwrap,@sandbox(DOMmeanscontent),@noframes.@requireand@resource name url: fetched once when you install or update the script and stored with it, never while pages load. A#sha256=,#md5=(or SRI style#sha256-...) hash after the address has to match, or the install stops. Each file may be 5 MB, and all of a script's files 10 MB.@connect: hostsGM_xmlhttpRequestandGM_downloadmay reach (self,localhost,*, an IP, a domain and its subdomains).@updateURLand@downloadURL: where update checks look.@antifeature: shown as a warning before installing.- Noodle's own
@noodle-tool-url(see Tools for agents).
A script without any @grant line gets the GM functions its code mentions, as in Tampermonkey. One that mentions none runs like @grant none.
Scripts run in the top-level page only, not in frames. The source may be 2 MB.
Where a script runs
As in Violentmonkey, a script runs in the page's own world by default, so unsafeWindow is the page's window and the script sees the page's variables. With @inject-into content it runs in an isolated world of its own: it shares the page's DOM but not its JavaScript. On pages that enforce Trusted Types (YouTube is one), scripts without an @inject-into line run in the isolated world, so setting innerHTML keeps working.
The page can tamper with code in its own world, since its scripts ran first. So a script granted anything beyond GM_info, GM_addStyle, GM_addElement, GM_log, menu commands, resources, unsafeWindow and the window.* grants runs in its isolated world unless it says @inject-into page. That covers requests, cookies, downloads, stored values, the clipboard, tabs, notifications and agent tools. In the isolated world unsafeWindow is the isolated window. The install card warns when such a script asks for the page.
@unwrap runs the source as it is, in the page, with no GM functions.
@run-at document-start runs before any of the page's own scripts, and Noodle waits for the <html> element first, so document.documentElement is there.
GM API
GM_info, GM_addStyle, GM_addElement and GM_log are always there (@grant none gets GM_info only). The rest need a @grant line. Either spelling unlocks both, so @grant GM.getValue also gives GM_getValue.
- Values:
GM_getValue,GM_setValue,GM_deleteValue,GM_listValues,GM_getValues,GM_setValues,GM_deleteValues,GM_addValueChangeListener(changes from other tabs arrive withremoteset to true),GM_removeValueChangeListener. Storage is per script, kept in the encrypted profile: 1 MB per value, 5 MB per script. GM_xmlhttpRequest: method, headers, data (text, FormData, Blob, ArrayBuffer, URLSearchParams),responseType(text,json,blob,arraybuffer,document), timeout, anonymous, cookie, user and password, overrideMimeType, redirect, and theonloadstart,onprogress,onreadystatechange,onload,onerror,ontimeout,onabortandonloadendevents. It returns a handle withabort(). Requests run from Noodle itself on the browsing session, so they carry your cookies unlessanonymous: true. Responses are capped at 16 MB and redirects at 10 hops.GM_download: saves to the Downloads folder and shows in the downloads list. It takes http addresses (checked against@connectlike requests),blob:anddata:URLs. Files that run when opened (.exe,.bat,.js,.lnkand the like) are refused, and on Windows saved files are marked as coming from the internet.GM_getResourceText,GM_getResourceURL(ablob:URL, or adata:URL when the second argument is false).GM_setClipboard,GM_notification(text, title, image, silent, timeout,onclick,ondone),GM_openInTab(active,insert; the handle hasclose(),closedandonclose).GM_registerMenuCommand,GM_unregisterMenuCommand.GM_getTab,GM_saveTab,GM_getTabs: an object per tab that lasts as long as the tab.GM_cookie(list,set,delete), for the sites the script runs on.unsafeWindow,window.onurlchange(anurlchangeevent when a single-page site changes the address),window.close,window.focus.- The promise versions under
GM., includingGM.xmlHttpRequest,GM.download,GM.getResourceUrl,GM.notification,GM.getTabandGM.getTabs.
GM_info has scriptHandler set to Noodle, the version, injectInto, platform, uuid, scriptMetaStr and every header field under script. A script that needs to know where it runs can check it:
if (GM_info.scriptHandler === 'Noodle') {
console.log('Running in Noodle', GM_info.version)
}
Support compared with the other managers
| Function or header | Noodle | Tampermonkey | Violentmonkey |
|---|---|---|---|
GM_addStyle, GM_addElement |
yes | yes | yes |
| Values, bulk values, change listeners | yes | yes | yes |
GM_xmlhttpRequest with @connect |
yes | yes | yes |
GM_download |
yes, without saveAs |
yes | yes |
GM_getResourceText, GM_getResourceURL |
yes | yes | yes |
GM_notification, GM_setClipboard |
yes | yes | yes |
GM_openInTab |
yes | yes | yes |
| Menu commands | yes | yes | yes |
GM_getTab, GM_saveTab, GM_getTabs |
yes | yes | no |
GM_cookie |
yes | yes | no |
window.onurlchange |
yes | yes | no |
GM_webRequest |
no | yes | no |
@inject-into, @unwrap |
yes | no | yes |
@run-at context-menu |
yes | yes | no |
| Scripts in frames | no, top-level pages only | yes | yes |
noodle.registerTool (agent tools) |
yes | no | no |
| Track local file | yes | no | yes |
Asking before contacting other hosts
GM_xmlhttpRequest and GM_download may reach the page's own host when the header lists @connect self, and the hosts the @connect lines name. For any other host Noodle shows a dialog: Allow once, Always allow, or Deny. Deny holds for the rest of the session, so a script can't keep asking. Always allow is saved with the script.
Redirects are checked hop by hop before each one is sent. A hop to a host that @connect or an earlier answer allows is followed; a hop to a host that would need the dialog is refused, since the dialog can't open in the middle of a redirect.
Debugging
Errors a script throws show on its row in Settings with the time, and in its run log (More > the list under the facts). The log keeps the pages the script ran on and its errors since Noodle started, 50 entries per script.
GM_log(...) writes to the page's console with the script's name in front:
GM_log('found', document.querySelectorAll('a').length, 'links')
// [Starter: reading time] found 212 links
Open the page's developer tools with F12. Each script is labelled userscript:<name>.user.js in the Sources panel, so you can set breakpoints in it. A script in its isolated world runs in a separate JavaScript context: pick it in the console's context menu (the dropdown above the console that says "top") to evaluate code next to the script.
The userscripts button in the top bar shows how many scripts ran on the page. Its menu lists them with their commands, turns a script off on that site or everywhere, and opens Settings. A script turned off on a site lists the site on its row, with a button to run it there again.
Updates
Check now in Settings looks for new versions, and with "Check for new versions once a day" on, Noodle does it daily. A new version that asks for nothing more (no new sites, no dropped excludes, no new grants, @connect hosts or files, and the same update addresses) and comes over https installs on its own. Any other waits on the script's row until you review it. A version you skip isn't offered again by the daily check.
Tools for agents
A script with @grant noodle.tools gets a noodle object with one function, registerTool:
noodle.registerTool({
name: 'word_count',
description: 'Counts the words on the page',
inputSchema: { type: 'object', properties: {} },
handler: async () => ({ words: document.body.innerText.split(/\s+/).length })
})
inputSchema is a JSON Schema object for the tool's arguments; the handler gets them as its first argument. Return any value that survives JSON.stringify. A script may register 20 tools.
When agent access is on, MCP clients see the tool as scriptname.toolname: the script name lowercased, with other characters turned into _, so the example in a script named "Page stats" is page_stats.word_count. A call runs the handler in an open tab the script matches. If there is none, Noodle opens @noodle-tool-url in the Agent space and waits up to 15 seconds for the tool to register. The handler has 60 seconds to answer, and its return value goes back to the agent as JSON.
Every call asks you first, like other sensitive agent actions, and "Allow on this site" covers the rest of the session for that tool. Noodle learns a script's tools when the script runs, so visit a page it matches once after installing or editing it. youtube-transcript.user.js in the examples is a full tool script. See agents.md for the rest of the agent side.
Limits
- Scripts run in top-level documents only;
@noframesis implied. GM_info.injectIntostill sayspageafter the Trusted Types fallback moved a script to its isolated world.GM_addElement('script')follows the page's Content Security Policy.GM_downloadignoressaveAsandconflictAction.- Value change listeners don't fire for values you change from outside the script (an import does notify them).
- The run log is in memory only.
- There is no
GM_webRequest, cloud sync or per-script@matchoverride.